laravel-linear maintained by labrodev
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