dniccum/linear-sdk
A PHP SDK for the Linear API that simplifies working with Linear's GraphQL endpoint. It provides convenient, structured methods for querying issues, projects, teams, and related resources, so you can integrate Linear data into your app without hand-writing GraphQL queries.
The package is a Spark-style integration: one trait on the workspace owner, one trait on the model that becomes an issue, and Linear does the rest.
composer require dniccum/linear-sdk
php artisan vendor:publish --tag=linear-migrations
php artisan migrate
php artisan vendor:publish --tag=linear-assets
Then, in order:
config/linear.php. The redirect URI must match /linear/callback (or whatever the config publishes) exactly — PKCE will fail silently-ish otherwise.HasLinearConnection to the owner model — usually User, or Team if your app is multi-tenant.CreatesLinearIssues to any model that should be filed, e.g. a Ticket or BugReport. You immediately get linear_issue_url, linear_issue_identifier and linear_sync_status columns plus sendToLinear(), retryLinear() and commentOnLinear()./linear and connect a workspace, then pick team/project/status/assignee/labels/priority.The first use case worth wiring up is the dullest one: a user submits a support form, and a Linear issue appears in the triage queue with the user's name and a link back. That's created only, automatic mode, no custom UI — twenty minutes of work.
Where to look first: the README for the config keys, then docs/http-contract.md if you plan to build your own UI, and the trait source itself — linearEvents() and the title/description methods are the two things you'll override most.
Scope the lifecycle events per model. Don't accept the defaults blindly; updated on a chatty model will spam Linear.
public function linearEvents(): array
{
return ['created']; // or ['created', 'updated', 'deleted']
}
public function linearTitle(): string
{
return "[{$this->reference}] {$this->subject}";
}
Automatic vs manual sending. Automatic mode suits internal bug/ticket flows. For anything user-facing where noise is expensive (draft content, bulk imports), switch to manual and call sendToLinear() from a controller action or an "Escalate to Linear" button. retryLinear() is the escape hatch for failed rows surfaced on /linear.
Multi-tenancy. Put HasLinearConnection on Team rather than User when each customer org has its own Linear workspace. Per-owner presentation is settable: Linear::backUsing('/settings').
Notifications and side effects. Don't poll linear_sync_status. Listen to LinearIssueCreated, LinearIssueFailed and LinearCommentDelivered — post to Slack on failure, or stamp an internal audit record on success. LinearIssueFailed is your only reliable signal, because lifecycle hooks deliberately swallow exceptions.
Embedding rather than redirecting. <x-linear::settings /> drops the same settings page into your existing layout. That's usually better UX than shipping users to a bare /linear route — and you keep the branded shell.
Headless / custom UI. Use the bundled Actions and typed DTOs (Team, Project, WorkflowState, Member) — they already expose Linear's colors, icons and avatar logic. If you're building a React/Vue front end, docs/http-contract.md is the contract to code against, and you can Linear::ignoreRoutes() to turn off the package's own route groups.
Testing. This is the part that makes day-to-day work pleasant:
Linear::fake();
$this->post('/tickets', [...])->assertRedirect();
Linear::assertIssueCreated(fn ($issue) => $issue->title === 'Broken login');
Linear::failWith('rate limited');
$this->artisan('queue:work --once');
Linear::assertIssueFailed('rate limited');
Pair that with Queue::fake() when you only want to assert dispatch, and use Linear::fake() plus a real queue worker in integration tests when you need to verify the job payload.
Re-publish the compiled assets on every update. They land in public/ and Vite hashes filenames; stale assets break the settings page in confusing ways. Add this now:
"post-update-cmd": [
"@php artisan vendor:publish --tag=linear-assets --force"
]
Publish the migrations. The release notes are explicit: without them, every save of a linked model logs a query error while still succeeding. That's a log-flooding bug that's easy to miss in local dev with a tolerant log level.
Lifecycle hooks never throw. This is a feature, but it means a misconfigured team ID won't blow up your test suite — it'll just quietly fail syncs. Assert on linear_sync_status, watch LinearIssueFailed, and check the retry list on /linear after deploying config changes.
Idempotency only covers timeouts. If a request times out after Linear created the issue, the retry adopts the existing issue rather than duplicating. It won't save you from double-dispatch caused by your own code (e.g. an observer registered twice, or a queued job retried after a created event fired twice). Guard your own triggers.
Queues must actually be running. Issues and comments go through queued jobs with retries and backoff. In dev with QUEUE_CONNECTION=sync everything looks instant, which hides ordering problems that surface in production. Test against a real async driver at least once.
Token refresh is lock-protected — the lock needs a shared store. On multi-server setups this means Redis/database cache, not array. With array you'll see intermittent 401s under load as several workers refresh simultaneously. Also note that API keys are stored encrypted: rotating APP_KEY without re-saving them will brick existing connections.
Soft deletes and pending syncs don't mix. A sync still queued when its model is soft-deleted fails with "no longer exists". If you soft-delete aggressively, either send synchronously on delete or accept the failure rows.
CSP. The settings page renders assignee avatars from Linear's CDN hosts. A strict img-src blocks them and you get the initials fallback — which is a graceful degradation, but users will notice their colleagues' faces vanish. Allow the hosts rather than loosening the policy.
SQL Server has a limitation on comment rows without a source model (one only). MySQL, Postgres and SQLite are unaffected. Worth a line in your deployment notes if you support MSSQL.
Version constraint is tight: PHP 8.3+ and Laravel 12 or 13. On a Laravel 11 codebase you cannot adopt this at all, so check before committing to the trait-based design in a spec.
Extension points to reach for before forking: published config, published views (override the settings blade), Linear::ignoreRoutes() for route conflicts, Linear::backUsing() for the back link, and container bindings for the Actions if you need to swap transport or add logging. The login route used on session expiry is configurable too — set it if your app doesn't use /login.
How can I help you explore Laravel packages today?