linear-reference-architecture
Production-grade Linear integration architecture patterns. Use when designing system architecture, choosing integration patterns, or reviewing architectural decisions for Linear integrations. Trigger: "linear architecture", "linear system design", "linear integration patterns", "linear best practices architecture".
Allowed Tools
Provided by Plugin
linear-pack
Claude Code skill pack for Linear (24 skills)
Installation
This skill is included in the linear-pack plugin:
/plugin install linear-pack@claude-code-plugins-plus
Click to copy
Instructions
Linear Reference Architecture
Overview
Production-grade architectural patterns for Linear integrations. Choose the right pattern based on team size, complexity, and real-time requirements.
Architecture Decision Matrix
| Pattern | Best For | Complexity | Rate Budget | Example |
|---|---|---|---|---|
| Simple | Single app, small team | Low | < 500 req/hr | Internal dashboard |
| Service-Oriented | Multiple apps, shared state | Medium | 500-2,000 req/hr | Platform with Linear sync |
| Event-Driven | Real-time needs, many consumers | High | < 500 req/hr + webhooks | Multi-service notification system |
| CQRS | Audit trails, complex queries | Very High | Minimal API calls | Compliance-grade tracking |
Architecture 1: Simple Integration
Direct SDK calls from your application. Best for scripts, internal tools, and prototypes.
// src/linear.ts — single module, shared client
import { LinearClient } from "@linear/sdk";
const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });
// Direct SDK calls from any part of your app
export async function getOpenIssues(teamKey: string) {
return client.issues({
first: 50,
filter: {
team: { key: { eq: teamKey } },
state: { type: { nin: ["completed", "canceled"] } },
},
orderBy: "priority",
});
}
export async function createBugReport(teamId: string, title: string, description: string) {
const labels = await client.issueLabels({ filter: { name: { eq: "Bug" } } });
return client.createIssue({
teamId,
title,
description,
priority: 2,
labelIds: labels.nodes.length ? [labels.nodes[0].id] : [],
});
}
Architecture 2: Service-Oriented with Gateway
Centralized Linear access through a gateway service with caching and rate limiting.
// src/linear-gateway.ts
import { LinearClient } from "@linear/sdk";
class LinearGateway {
private client: LinearClient;
private cache = new Map<string, { data: any; expiresAt: number }>();
private requestQueue: Array<{ fn: () => Promise<any>; resolve: Function; reject: Function }> = [];
private processing = false;
constructor(apiKey: string) {
this.client = new LinearClient({ apiKey });
}
// Cached reads
async getTeams() {
return this.cachedQuery("teams", () => this.client.teams().then(r => r.nodes), 600);
}
async getStates(teamId: string) {
return this.cachedQuery(`states:${teamId}`, async () => {
const team = await this.client.team(teamId);
return (await team.states()).nodes;
}, 1800);
}
// Rate-limited writes
async createIssue(input: any) {
return this.enqueue(() => this.client.createIssue(input));
}
async updateIssue(id: string, input: any) {
return this.enqueue(() => this.client.updateIssue(id, input));
}
// Custom queries through the gateway
async rawQuery(query: string, variables?: any) {
return this.enqueue(() => this.client.client.rawRequest(query, variables));
}
// Cache invalidation (called from webhook handler)
invalidate(pattern: string) {
for (const key of this.cache.keys()) {
if (key.startsWith(pattern)) this.cache.delete(key);
}
}
private async cachedQuery<T>(key: string, fn: () => Promise<T>, ttlSec: number): Promise<T> {
const cached = this.cache.get(key);
if (cached && Date.now() < cached.expiresAt) return cached.data;
const data = await this.enqueue(fn);
this.cache.set(key, { data, expiresAt: Date.now() + ttlSec * 1000 });
return data;
}
private async enqueue<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.requestQueue.push({ fn, resolve, reject });
if (!this.processing) this.processQueue();
});
}
private async processQueue() {
this.processing = true;
while (this.requestQueue.length > 0) {
const { fn, resolve, reject } = this.requestQueue.shift()!;
try { resolve(await fn()); } catch (e) { reject(e); }
if (this.requestQueue.length > 0) {
await new Promise(r => setTimeout(r, 100)); // 10 req/sec max
}
}
this.processing = false;
}
}
export const gateway = new LinearGateway(process.env.LINEAR_API_KEY!);
Architecture 3: Event-Driven
Webhook-centric architecture. Minimal API calls, real-time processing.
// src/event-processor.ts
import express from "express";
import crypto from "crypto";
import { EventEmitter } from "events";
// Internal event bus
const bus = new EventEmitter();
// Webhook ingester
const app = express();
app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["linear-signature"] as string;
const body = req.body.toString();
const expected = crypto.createHmac("sha256", process.env.LINEAR_WEBHOOK_SECRET!)
.update(body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(body);
res.json({ ok: true });
// Emit to internal consumers
bus.emit(`${event.type}.${event.action}`, event);
bus.emit(event.type, event);
bus.emit("*", event);
});
// Consumer: Slack notifications
bus.on("Issue.update", async (event) => {
if (event.updatedFrom?.stateId && event.data.state?.type === "completed") {
await notifySlack(`Done: ${event.data.identifier} ${event.data.title}`);
}
});
// Consumer: Database sync
bus.on("Issue", async (event) => {
if (event.action === "create") await db.issues.insert(event.data);
if (event.action === "update") await db.issues.update(event.data.id, event.data);
if (event.action === "remove") await db.issues.softDelete(event.data.id);
});
// Consumer: Cache invalidation
bus.on("*", (event) => {
gateway.invalidate(event.type.toLowerCase());
});
Architecture 4: CQRS with Local State
Separate read and write paths. Full local state for complex queries, API for writes.
// Write side: mutations go through Linear API
async function createIssue(input: any) {
const result = await gateway.createIssue(input);
// Local state updated via webhook, not here
return result;
}
// Read side: queries against local database (no API calls)
async function getSprintVelocity(teamKey: string, sprints: number) {
return db.query(`
SELECT c.name, SUM(i.estimate) as velocity
FROM cycles c
JOIN issues i ON i.cycle_id = c.id AND i.state_type = 'completed'
WHERE c.team_key = ? AND c.completed_at IS NOT NULL
ORDER BY c.completed_at DESC
LIMIT ?
`, [teamKey, sprints]);
}
// Sync: webhook events keep local state fresh
// Full sync: daily consistency check (see linear-data-handling)
Project Structure
src/
linear/
gateway.ts # Rate-limited, cached API access
webhook-handler.ts # Signature verification + routing
event-bus.ts # Internal event distribution
cache.ts # TTL cache with invalidation
services/
issue-service.ts # Business logic
sync-service.ts # Data synchronization
config/
linear.ts # Environment config + validation
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Rate limit exceeded | Too many direct API calls | Route all calls through gateway |
| Stale cache | TTL too long, missed webhook | Webhook invalidation + periodic full sync |
| Event loss | Webhook delivery failure | Idempotent handlers + consistency checks |
| Schema drift | SDK version mismatch | Pin version, test upgrades in staging |