From Local First to User First
Almost every fix in this cutover was a deletion. Pending ledgers, gone. Optimistic overlay, gone. Sign-in hand-off, gone. The architecture got faster by needing less.
We didn't come back to local-first because of the manifesto. We came back because we kept profiling our loading screens.
Vibes DIY has one metric we treat as the product itself: time to first glance — the moment something app-shaped is on your screen. We blur-preview apps while the code is still streaming. We keep codegen engines warm per user. We start builds on the server before your browser finishes navigating. Waiting, anywhere, is a bug. And when you hunt waiting long enough, every trail ends at the same place: the network is in the critical path, asking permission to show you your own data.
Our architecture had drifted the way most do. The cloud database was the truth, and "local" was a pile of exceptions bolted on around it — a pending ledger for offline writes, an optimistic overlay so your own edits looked instant, a hand-off ceremony for what happens when an anonymous visitor signs in. Every one of those mechanisms was a wait with a costume on. Every one had its own bugs. We were maintaining five apologies for the same architectural decision.
So we inverted it. The store on your device is the database. Reads and writes hit it directly and succeed immediately. Sync is a background loop that replicates whatever channels the server says you're allowed to receive — the server still decides all access; your device just stops asking it for permission to render. Offline stops being a mode. It's just sync not caught up yet.
Get posts like this in your inbox
One email field. Real updates. No algorithm required.
What surprised us was how far the principle spread once we stopped treating it as a data feature and started treating it as a performance rule. Three stories from the cutover:
The one bit that saves your drafts. In this model, a write the server rejects silently converges back to server truth — no error dialogs, by design. Applied naively, that rule would delete exactly the writes we promise will "sync when you sign in": an anonymous visitor's queued edits. The fix was a single bit on the wire — not accepted because you have no identity yet versus refused by policy. The first stays your local truth and drains when you sign in. The second quietly yields. One enum, two values, and the difference between a drop box that works on airplane mode and a data-loss bug.
The service worker that never was. While testing offline we discovered our sandbox service worker had been silently broken for a week — an HTML-escaping helper had turned the registration script into a syntax error, in every browser, and every test stayed green because the tests checked that the markup existed, not that it parsed. Nobody notices a dead cache when the network is fast. Offline is where performance bugs stop hiding. That alone justified the airplane-mode habit: it's adversarial profiling.
The loading screen we deleted instead of fixing. Our festival schedule app fetched pickathon.com's schedule per user, cached it per user, retried on failure, and showed "Loading the schedule…" while it worked. Now a backend job fetches once a minute, diffs the content — no timestamps, no churn, zero writes when nothing changed — and puts events in the shared database like any other documents. The app reads local docs. We didn't improve the loading state; we removed the reason it existed.
The same logic kept cascading. App code now serves cache-first and revalidates in the background, so a warm boot is instant whether you're online or not — a deploy reaches you on your next load, which is exactly the deal your data already gets. Identity paints at time zero from whoever you were last time, and the server corrects it afterward if it must; we stopped making your favorites wait for an auth round-trip to render data that was already on your device. And the first frame an app shows is its hydrated frame — we deleted the code that could render before the data was ready, rather than teaching more components to apologize for it.
That's the pattern worth naming: almost every fix in this cutover was a deletion. Pending ledgers, gone. Optimistic overlay, gone. Sign-in hand-off, gone — signing in is just "your channel set grew." The architecture got faster by needing less.
Honesty section, because local-first has real edges: revoking someone's access stops them receiving your data — it doesn't reach into their device and erase what already synced. Concurrent edits resolve document-level, last writer wins. A rejected write converges away silently. And durable offline on every browser is still a moving target we're actively engineering. These are trades, and we'd rather name them than pretend a distributed system is magic.
"Local-first" describes the architecture. "User-first" is why it won: the fastest possible app is the one that already has everything it needs on your device — your data, your identity, your app — and treats the network as staff, not management. It syncs your changes out, brings the world in, freshens the code for next time. It works for you.
We got here chasing milliseconds. The ideology was downstream.