Local-First Web Development: 7 Best Architecture Tips

Local-first web development is essential if you have ever watched your app fail because the hotel Wi-Fi was doing that thing where it connects but nothing actually loads. I watched our app—this thing I was genuinely proud of—render a blank screen with a spinner. Then a timeout error. Then nothing.

I pulled out my phone, tethered to cellular, and got a shaky connection. The app loaded, but every click was a two-second wait. Create a task? Spinner. Move a task between columns? Spinner. I sat there thinking: we built a front end in React, a back end in Node, a Postgres database, a Redis cache, a GraphQL API with six resolvers just for the task board. All that infrastructure, and the damn thing can’t show me my own data without a round-trip to a server 3,000 miles away.

When your application fails due to poor connectivity, it creates a deeply frustrating user experience. In fact, designing for distressed users is a crucial part of modern UX, and a broken app on a shaky connection is exactly the kind of friction we need to eliminate. That was the night I started seriously looking into local-first web development.

I want to be upfront: I spent the first year dismissing this architecture as academic. I read the Ink & Switch “Local-First Software” paper when it came out in 2019 and thought, “Cool research, not practical for real apps.” I was wrong. The tooling in 2019 genuinely wasn’t ready. Seven years later, I’ve shipped three production apps using this exact architecture. Here is what I actually think about mastering local-first web development today.

1. What “Local-First Web Development” Actually Means

I need to clear something up because I keep having this conversation at meetups. Local-first web development is not offline-first. It’s not “add a service worker and call it a day.” It’s not a synonym for PWA.

  • Offline-first means your app handles network loss gracefully, but the server is still the source of truth. When the network comes back, the server wins.
  • Cache-first (service workers caching responses) is a performance optimization. You’re serving stale data faster, but you haven’t changed who owns the data.
  • PWAs are a delivery mechanism: installable, cached, push notifications. None of these is a data architecture.

In local-first web development, your user’s device holds the primary copy of their data. The app reads and writes to a local database. Renders instantly. Syncs with servers or other devices in the background. The server, when it exists, is a sync peer with some special authority (authentication, backup, access control). But it’s not the gatekeeper.

2. Knowing When to Avoid Local-First Web Development

I’ve watched too many developers get excited about a new architecture and shoehorn it into projects where it doesn’t belong.

When to avoid local-first web development:

  • Server-generated data: Analytics dashboards, social media feeds, search results. The server produces this data, so consuming it via API requests is completely fine.
  • Strong transactional consistency: Banking, payment processing, and inventory management. If two people try to buy the last item in stock, you need a single authoritative database with ACID guarantees. Eventual consistency will lose you money.

Where local-first web development shines: Note-taking, document editing, collaborative design tools, project management, and field apps with unreliable connectivity. It’s great for user-generated data that benefits from instant interaction and should survive the server going down.

3. Replicas, Not Requests: The Git Mental Model

If you’ve used Git, you already understand the mental model. SVN was centralized—if the server was down, you couldn’t commit. Git gave every developer a full clone. You commit locally, branch locally, and merge locally.

Local-first web development is essentially Git for application data.

In a traditional architecture, adding a task looks like this:

  1. POST to API.
  2. Wait for the response.
  3. If success, update the local state.
  4. If failure, show an error toast.

In the local-first version, it is: write to local SQLite, done. The UI updates instantly because it’s reading from the same local database. Sync happens whenever. No loading state, no error handling for the write itself, and no optimistic update logic.

4. Where Data Lives on the Client

Forget localStorage. It’s synchronous, caps at 5-10 MB, and only stores strings. IndexedDB is asynchronous and can handle hundreds of megabytes, but its API is miserable to work with.

The real story today is SQLite compiled to WebAssembly (WASM). SQLite compiled to WASM, persisted to the Origin Private File System (OPFS), gives you a real relational database in the browser. Full SQL queries. Transactions. Indexes.

Here’s roughly what initialization looks like in a real project using wa-sqlite:

JavaScript

import { SQLiteAPI } from 'wa-sqlite';
import { OPFSCoopSyncVFS } from 'wa-sqlite/src/examples/OPFSCoopSyncVFS.js';

async function initDatabase() {
  const module = await SQLiteAPI.initialize();
  const vfs = new OPFSCoopSyncVFS('pm-tool-db');
  await vfs.initialize(module);

  const db = await module.open_v2('workspace.db');

  // HACK: wa-sqlite doesn't handle concurrent writes well on Safari,
  // so we serialize through a queue.
  await module.exec(db, `PRAGMA journal_mode=WAL`);

  await module.exec(db, `
    CREATE TABLE IF NOT EXISTS tasks (
      id TEXT PRIMARY KEY,
      title TEXT NOT NULL,
      status TEXT DEFAULT 'backlog',
      assignee_id TEXT,
      project_id TEXT NOT NULL,
      position REAL DEFAULT 0,
      created_at TEXT DEFAULT (datetime('now')),
      updated_at TEXT DEFAULT (datetime('now'))
    )
  `);

  return db;
}

5. Handling Conflicts in Local-First Web Development

Storing data locally is a solved problem. Syncing it reliably across devices and users is where you earn your gray hairs. There are two major approaches in local-first web development:

  1. CRDTs (Conflict-Free Replicated Data Types): Data structures designed so concurrent edits can always be merged without conflicts. Yjs is excellent for real-time collaborative text editing.
  2. Database Replication: Replicate rows between a server database (Postgres) and a client database (SQLite) with a sync engine managing the plumbing. Tools like PowerSync and ElectricSQL excel here.

Conflicts happen when two replicas modify the same data without seeing each other’s changes. What actually works for most cases is last-write-wins (LWW) at the field level, not the record level.

JavaScript

interface FieldValue {
  value: string | number | boolean;
  updatedAt: string;
  clientId: string; // Tiebreaker when timestamps match
}

function pickWinner(a: FieldValue, b: FieldValue): FieldValue {
  const timeA = new Date(a.updatedAt).getTime();
  const timeB = new Date(b.updatedAt).getTime();
  if (timeA !== timeB) return timeA > timeB ? a : b;
  return a.clientId > b.clientId ? a : b;
}

6. Schema Migrations On A Thousand Devices

On the server, you run a migration against one database. In local-first web development, every user has their own database that might be running any version of your schema.

Design your migrations to be additive. Add new columns with defaults or 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.

7. The Future of Local-First Web Development

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 dissolves. I’m also watching the convergence of AI and local-first web development—running models locally and keeping data on-device is a massive privacy advantage.

If you’re starting out, pick one feature in your current app that would benefit from instant local reads and offline writes. Add a local SQLite database. Wire up reactive queries. See how it feels. I think you’ll have the same reaction I did: this is how it should have always worked.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Stay Connected

3,452FansLike
2,345FollowersFollow
3,245FollowersFollow
- Advertisement -spot_img

Latest Articles