Claude-code-plugins-plus linear-reference-architecture
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/linear-pack/skills/linear-reference-architecture" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-linear-reference-architecture && rm -rf "$T"
manifest:
plugins/saas-packs/linear-pack/skills/linear-reference-architecture/SKILL.mdsource content
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 |