Looking to hire Laravel developers? Try LaraJobs

laravel-linear maintained by labrodev

Description
Stateless Linear GraphQL and OAuth 2 client for Laravel.
Author
Last update
2026/07/19 13:39 (dev-main)
License
Downloads
1

Comments
comments powered by Disqus

Laravel Linear

A stateless Linear GraphQL + OAuth 2 client for Laravel.

The package holds no tokens and no state of its own. Your application stores the token set wherever it wants (encrypted columns, a vault, ...) and passes the access token into every call. Linear access tokens expire after roughly 24 hours, and refresh tokens rotate on every refresh — always persist the full pair returned by refresh().

Requirements

  • PHP 8.4+
  • Laravel 13+

Installation

composer require labrodev/laravel-linear

Publish the config file if you want to customize it:

php artisan vendor:publish --tag=linear-config

Add your Linear OAuth application credentials to .env:

LINEAR_CLIENT_ID=your-client-id
LINEAR_CLIENT_SECRET=your-client-secret
LINEAR_REDIRECT_URI=https://your-app.test/linear/callback

Create the OAuth application at Linear under Settings -> API -> OAuth applications, using the same callback URL.

OAuth flow

Labrodev\Linear\LinearOAuthClient implements the full authorization-code flow against Linear (https://linear.app/oauth/authorize and https://api.linear.app/oauth/token).

1. Redirect the user to Linear

use Labrodev\Linear\LinearOAuthClient;

$state = bin2hex(random_bytes(16)); 

return redirect()->away(
    app(LinearOAuthClient::class)->authorizeUrl(state: $state),
);

The URL requests the read,write scope, actor=user and prompt=consent.

2. Exchange the callback code

$linearTokenSet = app(LinearOAuthClient::class)->exchangeCode(
    code: $request->string('code')->toString(),
);

// Persist all three values:
$linearTokenSet->accessToken;   // string, valid ~24h
$linearTokenSet->refreshToken;  // string, single use — rotates on refresh
$linearTokenSet->expiresAt;     // CarbonImmutable

3. Refresh when the access token expires

use Labrodev\Linear\Exceptions\LinearInvalidGrant;

try {
    $linearTokenSet = app(LinearOAuthClient::class)->refresh(
        refreshToken: $storedRefreshToken,
    );

    // Store the ROTATED pair — the old refresh token is now dead.
} catch (LinearInvalidGrant) {
    // The refresh token is no longer valid: the user must re-authorize.
}

4. Revoke on disconnect

app(LinearOAuthClient::class)->revoke(accessToken: $storedAccessToken);

GraphQL client

Labrodev\Linear\LinearClient talks to https://api.linear.app/graphql with the access token you pass in.

use Labrodev\Linear\LinearClient;

$linearClient = app(LinearClient::class);

// List teams — returns list<Labrodev\Linear\LinearTeam> (id, name, key)
$teams = $linearClient->teams(accessToken: $accessToken);

// Create an issue — returns the Linear issue id
$issueId = $linearClient->issueCreate(
    accessToken: $accessToken,
    teamId: $teams[0]->id,
    title: 'Monitor down: example.com',
    description: 'The monitor stopped responding at 12:00 UTC.',
    priority: 1,
);

// Close an issue — moves it to the team's "completed" workflow state.
// Silent no-op when the team has no completed state.
$linearClient->issueClose(accessToken: $accessToken, issueId: $issueId);

Exceptions

All exceptions carry fixed messages — tokens, codes and response bodies are never interpolated, so they are safe to log.

LinearOAuthFailed - Missing client config, or a failed exchange/refresh/revoke call. LinearInvalidGrant - The refresh token is dead; the user must re-authorize. LinearAuthFailed - Linear rejected the access token on a GraphQL call. LinearRequestFailed - A GraphQL call failed for a non-authentication reason.

Testing

composer test      # pest
composer phpstan   # larastan, level 7
composer pint      # laravel preset
composer rector    # laravel preset
composer check     # all of the above

License

MIT - see LICENSE.md.

Credits

Petro Lashyn, https://lshnpt.dev