There is no Linear kernel CVE
Linear updates an issue in milliseconds by putting the database in the browser. A breakdown of its local-first sync engine, bundling, and auth.
Why Linear updates issues in milliseconds
Updating an issue in Linear takes a few milliseconds. The same operation in a conventional CRUD app runs around 300ms, because the app registers the click, fires an HTTP request, waits for a server to query a database, and repaints when the response lands. Linear removes the wait by putting the database the UI reads from into the browser.
What follows comes from a public technical breakdown by a developer who has used Linear since its beta and reverse-engineered the shipped client. They state up front that they have never worked at Linear or seen the code, so treat the build-pipeline figures (which Linear published on its own blog) as vendor claims and the rest as observed behavior.
The database is in the browser
Linear’s UI reads from IndexedDB, not from a server on every interaction. A mutation applies locally first, then pushes to the server asynchronously, and the server broadcasts deltas to other clients over a WebSocket. The code difference is blunt. A conventional update shows a spinner, awaits a PATCH, parses the response, sets state, and hides the spinner. Linear’s version is two lines:
issue.title = "Faster app launch";
issue.save();
The first line mutates an in-memory MobX observable, so the UI re-renders synchronously. The second queues a transaction that the sync engine batches and flushes to the server. There is no spinner because nothing is being waited on.
Co-founder Tuomas, speaking at a 2024 conference: “Literally the first lines of code that I wrote was the sync engine, which is very uncommon to what you usually do when you’re a startup.” You don’t need a custom sync engine to get most of this. For ordinary apps, Tanstack Query or SWR with optimistic updates reach the same feel: update local state immediately, validate in the background, roll back only if the server disagrees.
The stack is deliberately boring
React, TypeScript, MobX, Postgres, a CDN. No edge database, no React Server Components. The backend is Node on Kubernetes on GCP, with Postgres on Cloud SQL (the issues table partitioned 300 ways), Redis for the event bus, cache and sync cursors, turbopuffer as a vector DB for similar-issue detection, and Cloudflare Workers as a multi-region edge proxy. Desktop is Electron running the same web JavaScript, iOS is Swift, and Android is a separate Kotlin reimplementation. The app itself is client-side rendered, which usually gets blamed for slow initial loads.
Making first load feel instant
Linear has rewritten its build pipeline four times: Parcel, then Rollup, then Vite, then Rolldown (as of mid-2025, Rolldown-Vite with plugin-react-oxc). Linear’s blog posts claim the migrations cut shipped code by 50%, shrank the compressed bundle by 30%, made cold-cache loads 10 to 30% faster, dropped time-to-first-paint on the active-issues view by 59% on Safari, and cut memory use by 70 to 80%. Most of that came from targeting only modern browsers (no polyfills, no ES5 transpilation, no nomodule fallback), better dead-code elimination, and aggressive code splitting.
The client still ships roughly 21 MB of minified JavaScript, split into hundreds of route-level chunks fetched on demand, with one chunk per npm package above about 3 KB. Bumping a single dependency invalidates one chunk instead of a monolithic vendor.js.
Splitting into hundreds of chunks creates a waterfall risk, because the browser can’t see a chunk’s imports until it parses the entry script. Linear’s index.html lists the critical chunks as modulepreload links in the head, so the browser fires those fetches in parallel before any script runs. The crossorigin attribute on each preload matches the entry script’s, so the preloaded fetch gets reused instead of fetched twice. Everything else, the chunks for views you haven’t opened yet, gets pulled in the background by a service worker whose precache manifest lists around 1,200 hashed assets. Within seconds of hitting the login screen the full app is in cache, later navigations skip the network entirely, and combined with the data already in IndexedDB the app keeps working offline.
Fonts get the same treatment: Inter Variable as a single woff2 covering the 100 to 900 weight axis, font-display: swap, and crossorigin="anonymous" on the preload so the preloaded font isn’t fetched a second time when CSS references it.
Render first, authenticate second
The head of index.html inlines enough CSS to paint the app shell with no external stylesheet, plus a boot script that reads localStorage.splashScreenConfig and applies the remembered sidebar width, colors, and dark mode before any bundle parses. The same script decides what to render by checking whether the local store exists:
if (localStorage.getItem("ApplicationStore") === null) {
document.documentElement.classList.add("logged-out");
}
If the store is present, the user has used Linear in this browser, their workspace is already in IndexedDB, and the app renders immediately. If it’s missing, there’s nothing to show, so the shell switches to the login layout. The real session token lives in a cookie, and the client never validates it at boot. It renders what it has and lets the next network call, the WebSocket handshake or a sync delta or any HTTP request, be the thing that returns 401 if the session has gone stale, at which point the client redirects to login.
One thing to flag for anyone copying this: the client decides what to display from local state and treats the server as the authority only for correctness, reconciled after the render. For a productivity tool that’s a reasonable call. The security boundary sits entirely in the server responses that follow, not in the render, and the 401-driven redirect is the only thing stopping a client with a dead session from continuing to show workspace structure it has cached.
The sync engine
Three properties produce the speed. First, the data is already local: on boot the client hydrates IndexedDB into an in-memory MobX object pool and queries hit the pool, not the server. The two heaviest tables, Issue and Comment, lazy-hydrate on demand, so a 10,000-issue workspace boots about as fast as a 100-issue one and startup cost tracks workspace structure rather than size. Second, mutations don’t wait: a change updates the observable, writes to a durable transaction queue in IndexedDB, and queues for the server all at once, and if the server rejects it the observable reverts with a brief flicker, which the breakdown says rarely happens because most invalid mutations are caught before the transaction is created. Third, a confirmed mutation (yours or another client’s) comes back as a small JSON envelope describing what changed, and the client applies it by writing the single observable it touches. Because every model property is its own observable and every reading component is wrapped in observer(), only the components bound to that one value re-render.
Keep Reading
data breachA February 2025 migration exposed DigiCert customer records online
A February 2025 server migration exposed a DigiCert internal system, and a self-registered account read archived customer data and identity documents.
OpenSSHLong Passwords Bypass Alpine SSH Authentication
OpenSSH 10.6p2 fixes a critical password authentication bypass affecting Alpine Linux and other potentially vulnerable musl systems.
opensshLong passwords unlock disabled accounts
The "*sshole" flaw lets oversized passwords log into *-disabled accounts on musl-based OpenSSH servers like Alpine under certain configurations. Fixed in 10.6p2.
Latest on the Wire
Full wire →- Bending Spoons Destroys Harvest With 10x Price Hike and LLM SupportHacker News
- CISA Flags SonicWall MFA Bypass Amid Exploitation ReportsThe Hacker News
- Claude Opus Ports 3D Models from Three.js to Vanilla WebGLSimon Willison
- Collapsing AI PR descriptions with HTML details tagsSimon Willison
New signal daily · RSS
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.