Linear Local Dev Loop
Overview
Set up an efficient local development workflow for building Linear integrations. Covers project scaffolding, environment config, test utilities, webhook tunneling with ngrok, and integration testing with vitest.
Prerequisites
- Node.js 18+ with TypeScript
@linear/sdk package
- Separate Linear workspace or team for development (recommended)
- ngrok or cloudflared for webhook tunnel testing
Instructions
Step 1: Project Scaffolding
set -euo pipefail
mkdir linear-integration && cd linear-integration
npm init -y
npm install @linear/sdk dotenv
npm install -D typescript @types/node vitest tsx
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --strict
Step 2: Environment Configuration
cat > .env << 'EOF'
LINEAR_API_KEY=lin_api_dev_xxxxxxxxxxxx
LINEAR_WEBHOOK_SECRET=whsec_dev_xxxxxxxxxxxx
LINEAR_DEV_TEAM_KEY=DEV
NODE_ENV=development
EOF
cat > .env.example << 'EOF'
LINEAR_API_KEY=lin_api_your_key_here
LINEAR_WEBHOOK_SECRET=
LINEAR_DEV_TEAM_KEY=DEV
NODE_ENV=development
EOF
echo -e ".env\n.env.local\n.env.*.local" >> .gitignore
Step 3: Client Module with Connection Verification
import { LinearClient } from "@linear/sdk";
import "dotenv/config";
let _client: LinearClient | null = null;
export function getClient(): LinearClient {
if (!_client) {
const apiKey = process.env.LINEAR_API_KEY;
if (!apiKey) throw new Error("LINEAR_API_KEY not set — copy .env.example to .env");
_client = new LinearClient({ apiKey });
}
return _client;
}
export async function verifyConnection(): Promise<void> {
const client = getClient();
const viewer = await client.viewer;
const teams = await client.teams();
console.log(`[Linear] Connected as ${viewer.name} (${viewer.email})`);
console.log(`[Linear] Teams: ${teams.nodes.map(t => t.key).join(", ")}`);
}
Step 4: Test Data Utilities
import { getClient } from "./client";
const TEST_PREFIX = "[DEV-TEST]";
export async function getDevTeam() {
const client = getClient();
const teamKey = process.env.LINEAR_DEV_TEAM_KEY ?? "DEV";
const teams = await client.teams({ filter: { key: { eq: teamKey } } });
const team = teams.nodes[0];
if (!team) throw new Error(`Team ${teamKey} not found — set LINEAR_DEV_TEAM_KEY`);
return team;
}
export async function createTestIssue(title?: string) {
const client = getClient();
const team = await getDevTeam();
const result = await client.createIssue({
teamId: team.id,
title: ,
: ,
: ,
});
result.;
}
() {
client = ();
team = ();
issues = client.({
: {
: { : { : team. } },
: { : },
},
: ,
});
deleted = ;
( issue issues.) {
issue.();
deleted++;
}
.();
}
Step 5: Integration Tests with Vitest
import { describe, it, expect, afterAll } from "vitest";
import { getClient } from "../src/client";
import { createTestIssue, cleanupTestIssues, getDevTeam } from "../src/test-utils";
describe("Linear Integration", () => {
afterAll(async () => {
await cleanupTestIssues();
});
it("authenticates successfully", async () => {
const client = getClient();
const viewer = await client.viewer;
expect(viewer.name).toBeDefined();
expect(viewer.email).toBeDefined();
});
it("creates and updates an issue", async () => {
const client = getClient();
const issue = await createTestIssue("vitest create");
expect(issue).toBeDefined();
expect(issue?.title).toContain("[DEV-TEST]");
client.(issue!., { : });
updated = client.(issue!.);
(updated.).();
});
(, () => {
team = ();
states = team.();
types = states..( s.);
(types).();
(types).();
});
});
Step 6: Package Scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"verify": "tsx src/verify-connection.ts",
"test": "vitest run",
"test:watch": "vitest --watch",
"cleanup": "tsx src/cleanup.ts"
}
}
Step 7: Webhook Local Development with ngrok
npm run dev
ngrok http 3000
Minimal webhook receiver for local testing:
import express from "express";
import crypto from "crypto";
const app = express();
app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => {
const body = req.body.toString();
const sig = req.headers["linear-signature"] as string;
const secret = process.env.LINEAR_WEBHOOK_SECRET!;
if (secret && sig) {
const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
if (sig !== expected) {
console.warn("Signature mismatch — check LINEAR_WEBHOOK_SECRET");
}
}
const event = JSON.parse(body);
console.log(`[Webhook] ${event.type}.${event.action}:`, event.data?.identifier ?? event.data?.);
res.({ : });
});
app.(, .());
Error Handling
| Error | Cause | Solution |
|---|
LINEAR_API_KEY not set | Missing .env | Copy .env.example to .env and fill in values |
Team DEV not found | Wrong team key | Set LINEAR_DEV_TEAM_KEY to a valid team key |
Cannot find module | TypeScript path issue | Check tsconfig.json module resolution |
| Webhook not received | Tunnel not running | Start ngrok http 3000 and register the URL |
Authentication required | Expired dev API key | Regenerate in Linear Settings > Account > API |
Examples
Quick Connection Test Script
import { verifyConnection } from "./client";
verifyConnection()
.then(() => console.log("Connection OK"))
.catch((err) => { console.error("Connection FAILED:", err.message); process.exit(1); });
Resources