Vibe code your own Linear
Vibe code your own Linear as an issue tracker. Title, status, a backlog. The sync engine is what you pay for. Public architecture, and which half I would ship.
You can vibe code your own Linear as an issue tracker. Title, status, assignee, a backlog. The catalog calls Linear a sharp tracker for the way your team already works.
You pay $10–$16 a person a month. The card says that price scales with the team. The verdict is “For one team, yes”.
Yesterday I wrote about building your own link shortener in 90 minutes. That was a regular backlog post. This one is a teardown: what is public about the architecture, which half you can vibe-code, and which half you should leave alone.
Vibe code your own Linear as a tracker
The Linear page splits the work into three rows.
The first row is 12 hours. Issues, statuses, a backlog view. That is a list with a few filters and a status column. You can vibe-code this.
The second row is 4 days. Cycles, a command menu, a throughput chart, GitHub links. Still a tracker. More screens, more keyboard work.
The third row is 3 months and up. Offline sync, real-time everything, roadmaps. The table labels that row as the part you should skip.
If you tell an agent to “clone Linear”, it aims at the third row. The useful v1 is the first one.
Here is the shape of the data. Nothing else has to exist at 12 hours.
const issue = {
id: 'LIN-184',
title: 'Fix timezone on the booking confirmation',
status: 'in_progress',
assignee: 'flavio',
cycle: null,
}
A team, an issue, a status. Comments can wait an afternoon. Labels are a string list. Cycles wait until you have closed a week of real work.
The audience line on the page is one product team shipping in weekly cycles. If that is you, and the team is small, a local tracker is enough. If the company already lives in Linear, keep paying. The file is the source of truth, the same way Slack is for chat.
Identifiers are the first real rule
The mini-prompt on the page is “ABC-123 forever”. LIN-184 is not a row number you reuse. Commit messages, pull requests, and human memory all point at it.
Each team has a short key and a counter. Creating an issue increments that counter in the same write that inserts the row. Two people hitting create must not mint the same number. Deleting LIN-87 retires it. Gaps are fine because reuse breaks old commit messages.
async function createIssue(db, teamKey, title) {
const number = await db.increment(`${teamKey}:issue`)
return {
id: `${teamKey}-${number}`,
title,
status: 'backlog',
}
}
await createIssue(db, 'LIN', 'Fix timezone on the booking confirmation')
//{ id: 'LIN-184', title: 'Fix timezone on the booking confirmation', status: 'backlog' }
Resolution is by identifier alone. The title can change daily. Old links still land.
Linear’s GraphQL docs let you fetch an issue by that short id (BLA-123) or by UUID. Mentions in Markdown are plain Linear URLs that the app later turns into @LIN-123. That is a product detail, not a hard problem. Store durable ids and never recycle them.
How does Linear work: the public API
Linear publishes the same GraphQL API they use internally. That is their sentence, not a guess.
The endpoint is https://api.linear.app/graphql. You authenticate with a personal key or OAuth. You query teams, issues, workflow states. You create and update issues with mutations.
The data model is not a secret. Issue, team, state, assignee, cycle, comment, label. The webhook docs list the same objects: issues, comments, labels, projects, cycles, users. A change payload includes action, type, data, and updatedFrom with the previous values.
Linear’s own API page says mutations made through the API are observed in real time by all clients. The clients are replicas of a workspace. They do not refetch a page of issues when someone changes a status.
If you are building a viewer on top of Linear, they tell you not to poll each issue. Register a webhook. Or, if you must poll, ask for recently updated issues. That advice is in the Fetching Updates section.
A vibe-coded tracker does not need that. One person, one browser tab, a form post, a list. Linear needs it because five people have the same issue open and they expect the assignee to flip without a refresh.
The sync engine is the other product
Tuomas Artman, Linear’s CTO, has given two public talks on this. The first was at React Helsinki. The second is on Linear’s site as Scaling the Linear Sync Engine (June 29, 2023). I am summarizing what he said, not reconstructing their protocol.
The client keeps an in-memory object graph. They use MobX to make it reactive. There is a local copy in IndexedDB. Writes go into the object graph first, then into a transaction queue that is also persisted locally. The queue is how a change survives a refresh or a dropped connection.
When the server accepts a write, other clients hear about it over a websocket and fold it into their local copy. From the UI’s point of view, issue.assignee just changed. Nobody wrote a fetch in that screen.
Artman has said this let them ship features by defining a model and building the UI. The network layer sat underneath. In the later talk he says the first version of that API stayed synchronous for about two years. It started to hurt around 50,000 objects in memory. Loading that many models on a modern machine takes something like 800 milliseconds to a second. They wanted more objects than that, and a faster first paint, so they made loading lazy while keeping the “just read the object” feeling.
I would not try to copy that stack. The 12-hour row does not need an object pool, a transaction queue, or a bootstrap dump. A D1 table and a full page render is the honest v1.
What I would copy is the product rule behind it: the client is a replica, and the server is the authority on order. Two people changing the same status do not merge the text of “In Progress” and “Done”. One write wins.
Artman has said they did not start with CRDTs. A write-up of the talks says they later used CRDTs only for issue descriptions, because two people editing the same structured field at once is rare. Linear has not published a protocol spec I can find, so treat that last part as second-hand. The first-hand claim is simpler. The server orders transactions. Last write wins on a property is enough for a tracker.
function applyRemote(local, change) {
if (change.syncId <= local.lastSyncId) return local
return {
...local,
lastSyncId: change.syncId,
issues: { ...local.issues, [change.issueId]: change.issue },
}
}
That is a sketch of the idea, not Linear’s code. Catch up from a number. Ignore old changes. Replace one issue. A real engine also has undo, partial indexes, and a way to bootstrap a laptop that has been closed for a week. That is the 3-month row.
Figma has a similar split. The canvas is one product. The multiplayer file is another. How Figma works is that teardown. Notion is the same story with blocks. Linear’s version is easier to see because they talked about it in public.
GitHub is two different jobs
The catalog puts “GitHub links” on the 4-day row and “deep Git synchronization” in the non-goals. That split matches Linear’s own docs.
The GitHub integration does two things.
The first is linking. You put Fixes ENG-123 in a pull request title or description, or you put the issue id in the branch name. Linear attaches the PR to the issue and can move the status as the PR is drafted and merged. That is a webhook and a regex. I would ship a version of this after the tracker works. Parse the id, store the PR url, flip the status. Do not sync comments.
The second is GitHub Issues Sync. A Linear team maps to a GitHub repo. New issues can flow one way (GitHub to Linear) or two ways. Comments, status, labels, and assignee can stay in sync. Two-way sync is one repo per Linear team. Linear says this only covers newly created issues. Historical ones go through an importer.
Two-way issue sync is the kind of integration that looks small and then eats a month. Status mappings drift. A label exists on one side. Someone edits a comment in both places. The catalog is right to leave it out of v1.
If the agent starts a full GitHub clone inside your tracker, stop it. Paste the non-goal again.
What I would actually rebuild
I would rebuild Linear for a team of three who currently bounce issues through a Notion table or a GitHub project board. Issues, statuses, a backlog. Durable ids. A comment thread. One weekly cycle if they already think in weeks.
I would host it next to the product. I would not build offline. I would not build a command menu on day one, even though Linear’s is famous. The mini-prompt for that menu is a weekend by itself: subsequence fuzzy matching, command chaining, a few thousand issues in under 50 milliseconds.
I would add a GitHub link field when the first PR is annoying to find. I would not sync GitHub Issues.
I would not rebuild Linear for a company that already treats it as the system of record. The sync engine, the keyboard culture, the roadmaps, the way every client agrees, that is the product they bought. You will spend the 3-month row and still open the paid app on Monday.
I use the same test for Slack. A campfire for a focused group is vibeable. A company search index is a different job.
Start with the first row
Go to Build your own Linear. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: issues, statuses, a backlog. Twelve hours.
If the team already lives in Linear’s realtime file, keep paying.