305 private links
The client is not a thin view requesting permission to show data. The client is a node in a distributed system with its own database.
It’s overkill for simple CRUD apps with no offline or collaboration needs.
But here’s where it shines: note-taking, document editing, collaborative design tools, project management, field apps with unreliable connectivity, basically anything where data privacy is a selling point, as well as anything with real-time collaboration.
One more thing I wish someone had told me earlier: you don’t have to go all-in. I’ve had the best results using local-first for specific features within otherwise traditional apps. Offline drafts in a blog editor. Real-time collaborative notes inside a project management tool that’s otherwise standard REST.
To do so: SQLite in the browser via WebAssembly; persisted to the Origin Private File System (OPFS). The author describes the method used.
To avoid conflicts: CRDT. Yjs exists. There is also Automerge and the newer Loro.
To grasp the data: replicate rows via database replication.
PowerSync does this well from Postgres to SQLite.
Triplit is a full-stack database with sync built-in.
LiveStore use an event-based approach.
TinyBase for prototyping or small apps.
PGLite (Postgres compiled to WASM) but it has a significant bundle size and memory footprint for mobile browsers.
Often the last-write-wins (LWW) is the best strategy at the field level.
For a document body, CRDT should be used.
To book a meeting, one must verify there is no other meeting booked by someone else. " The approach I’ve landed on (after getting it wrong twice) is: validate on the server during the write-back phase, but flag violations rather than silently rejecting them. When the client pushes mutations to the server during sync, the server runs them through a constraint validation layer before applying them to Postgres". See the example.
The conflict should then be resolved by the user.
For something like inventory management where two people “buy” the last item, that window is unacceptable, and that’s exactly why I said earlier that local-first is wrong for systems requiring strong transactional consistency.
Conflict resolution works well for texts with CRDT.
See such app architecture: https://www.smashingmagazine.com/2026/05/architecture-local-first-web-development/#building-a-real-app-architecture-auth-and-migrations
Example of E2E (local, on device) encryption for https://anytype.io/
One thing to consider is migrations: Design your migrations to be additive. New columns with defaults. New tables. Don’t rename or drop columns unless you absolutely must, because users running old app versions will still be syncing data, and your server needs to handle the mismatch. I learned this the hard way when I dropped a column that an older client was still writing to, which caused silent sync failures for about 200 users over a weekend.
Performance are awesome (< 10ms for read or writes). The initial sync is where the cost occurs.
The architecture can be tested with Playwright and context.setOffline(true).
I’m excited about where this is going. PGlite (full Postgres in the browser) feels like a glimpse of a future where the client/server data layer distinction just dissolves. You write SQL, it runs everywhere, sync is a runtime concern rather than an architectural decision. We’re not there yet, but you can see it from here.
There is also no standard for a sync engine. Migrating away a sync engine is not trivial. I’m also worried about the complexity budget. Local-first adds real architectural complexity: sync engines, conflict resolution, client-side migrations, partial replication, and auth at the sync boundary.
The author uses a service worker in wasm to render HTML. The service worker syncs the data with the server.
Note fetch requests can be intercepted with the ... fetch event in service workers
The service worker used here is written in Go. Note the "localfirst" approach runs only after the service worker is loaded. The initial page is loaded as simple HTML because of SSR. That's the advantage of WASM: the code runs on the client and the server.
(following https://shaarli.lyokolux.space/shaare/CosnyQ)
Automerge is a local-first sync engine for multiplayer apps that works offline, prevents conflicts, and runs fast.
How to build the local-first software with the most interoperable data system: files.
How to avoid conflict while syncing them on cloud providers? Tonsky relates some strategy.
A collection of resources such as guides, blog posts, advocacy, how to's
IndexedDB can be used to store a lot of data. It has some caveats though.
Storage:
About deletion, use soft delete to smoothen the synchronization if a user deletes a record and another one update it.
About record collection, use unique IDs (UUID v4) or property related ids with (UUID v5).
About ordering, it is easier to use fractional indices! Read more on https://www.figma.com/blog/realtime-editing-of-ordered-sequences/, or https://www.steveruiz.me/posts/reordering-fractional-indices, or use a dedicated library.
Sync is made with pull and push
Update:
- Send atomic changes from a client is the more convenient way. We can send only the model’s ID and its updated fields.
- send operations instead of changed data,
Conflict resolution:
- In some cases, last-write wins at the record field level will be enough
- in others, we strongly need a full-fledged CRDT.