labrodev / laravel-linear
Stateless Linear GraphQL and OAuth 2 client for Laravel.
Requires
- php: ^8.4
- guzzlehttp/guzzle: ^7.8
- illuminate/http: ^13.0
- illuminate/support: ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- driftingly/rector-laravel: ^2.5
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
README
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