Blog

Local-First Architecture for Enterprise Web Apps: Beyond Offline Fallbacks

Discover how local-first architecture uses OPFS, PGlite, and relational sync engines to deliver sub-millisecond latency and resilience in enterprise apps.

About this article

  • Published on September 21, 2026
  • 8 min read
local-firstenterprise-architecturesqlite-wasmpglitedistributed-systems

Traditional web applications rely on the assumption that a stable, low-latency network connection is always available. Most line-of-business software treats a central cloud database as the single source of truth, routing mutations and lookups through REST or GraphQL endpoints. When latency spikes or connectivity drops (such as in warehouses, field service inspections, or aircraft hangars), users encounter loading spinners, locked forms, and broken optimistic updates.

Local-first architecture changes this model by treating the client's local datastore as the primary source of truth instead of treating the browser as a thin client for remote state. Reads and mutations run against an in-process database locally in under 1ms, while a background sync engine reconciles state bidirectionally with the server.

The concept stems from the 2019 Ink & Switch research paper "Local-First Software". Between 2024 and 2026, enterprise adoption moved from experimental prototypes to production deployments. Standardized browser filesystem access, WebAssembly relational runtimes, and specialized sync protocols now let teams run distributed web systems without giving up transactional guarantees or security.

At Lidex, we build custom software and modernize enterprise systems where network-induced downtime disrupts core operations. Below is an overview of how local-first systems work under the hood, their practical trade-offs, and implementation patterns for production.

Why offline fallback fails enterprise workflows (and how local-as-primary fixes it)

Most web applications try to handle intermittent connectivity using service worker caches and optimistic UI updates. When someone submits an invoice or changes inventory counts, the client updates its local state variable, assumes success, and sends an asynchronous request over the network. If that request fails, the application shows an error toast, rolls back the change, or pushes the mutation into an ad hoc IndexedDB queue.

In complex transactional environments, this pattern breaks down quickly:

  • State inconsistency: If a user completes three dependent steps while offline (such as creating an inspection report, attaching line-item defects, and assigning tasks), rolling back step two leaves the remaining state out of sync or corrupted.
  • Network latency: Waiting 100 to 300ms for network round trips between view updates slows down operator throughput in data-dense interfaces like point-of-sale or ERP systems.
  • Complex error handling: Front-end code ends up burdened with custom mutation queues, retry logic, exponential backoffs, and conflict prompts across forms.

With a local-as-primary design, reads and writes run synchronously against local memory or disk. Because the application writes directly to an embedded database, persistence is immediate and atomic. Mutations do not fail simply because the network drops. The network serves as an asynchronous transport layer for sync rather than a blocking execution dependency.

Traditional Architecture:
[Client UI] ---> Network (100-300ms) ---> [API Gateway] ---> [Cloud Database]
                      (Network drops = System stalls)

Local-First Architecture:
[Client UI] ---> Local Embedded DB (<1ms)
                      |
             Background Sync Engine
                      |
             [Central Database & Sync Server]

Browser storage substrates: OPFS, WASM SQLite, and PGlite

Running an embedded relational engine inside the browser was once restricted by IndexedDB. IndexedDB handles basic key-value data, but its API is clumsy, its transaction support is limited, and its cursor-based serialization makes querying thousands of records slow.

Modern implementations use the Origin Private File System (OPFS), supported across modern Chromium, Safari, and Firefox releases. OPFS gives web applications private filesystem access with fast read and write performance, allowing WebAssembly-compiled databases to run native filesystem operations inside Web Workers.

Two runtimes are common in production:

  1. SQLite compiled to WebAssembly (wa-sqlite or official SQLite WASM): Using OPFS for persistence, WASM SQLite provides transactions, indexes, and SQL queries directly on the client across hundreds of thousands of records.
  2. PGlite (ElectricSQL): Released in 2024 and maintained through subsequent major revisions, PGlite packages Postgres into a WebAssembly bundle of roughly 3MB compressed. It runs inside browser workers, Bun, or Node.js, offering PostgreSQL dialect compatibility. Clients can run parameterized queries, relational joins, and client-side vector search using embedded pgvector extensions.

Quota management and eviction safeguards

Client devices have finite storage. While modern browsers allow applications to request persistent storage via navigator.storage.persist() and use up to 60% to 80% of available disk space, the browser or host operating system can still evict data if disk space runs critically low.

Production local-first systems must not treat client storage as permanent archive storage. The application requires an authoritative server-side backup paired with a least-recently-used (LRU) eviction strategy to prune stale records while preserving the active working set locally.

The sync engine: why enterprise systems moved beyond pure CRDTs

Early local-first research focused heavily on Conflict-free Replicated Data Types (CRDTs). State-based and operation-based CRDTs allow multiple nodes to update data concurrently without a central coordinator, resolving divergent branches deterministically.

Libraries like Yjs, Automerge (notably after its 2025 Rust core rewrite, which significantly lowered document memory overhead), and Loro work well for collaborative rich-text and canvas tools like Notion, Figma, or Excalidraw. However, pure CRDTs introduce major friction when applied to relational business applications like ERP, CRM, and inventory systems:

  • Tombstones and memory growth: Tracking deletions and frequent updates in transactional data creates tombstone overhead that balloons memory usage.
  • Relational constraints: Business systems depend on foreign keys and multi-record invariants. Pure peer-to-peer CRDTs cannot easily enforce cross-table constraints without a coordinating authority.

Because of these limitations, teams building business software have largely adopted server-reconciled sync engines that connect a central database (typically Postgres) with local embedded client replicas.

Production sync frameworks

  • Zero (Rocicorp): Uses server reconciliation inspired by multiplayer game architectures. A zero-cache process sits in front of Postgres and syncs filtered query subscriptions to client-side query engines.
  • ElectricSQL (Shapes Architecture): Shifted away from pure client CRDTs toward an HTTP streaming model called Shapes. The server streams parameterized, cacheable subsets of Postgres tables over HTTP or Server-Sent Events (SSE) into local SQLite or PGlite instances.
  • PowerSync: A service that syncs backend databases (Postgres, MySQL, MongoDB) directly with local SQLite databases across web, Flutter, React Native, and Kotlin Multiplatform clients.
Lidex Soluções Digitais — technology that transforms businesses

Data slicing and security at the replication boundary

Enterprise databases routinely store tens or hundreds of gigabytes across millions of rows. Clients cannot copy the entire database locally, nor should they have unfettered query access to data outside their scope. Production setups manage this through partial replication and strict authorization boundaries.

Dynamic data slicing

Modern sync engines establish filtered subscriptions rather than syncing the full database. The sync boundaries adapt based on the user's active view and role.

For example, in Rocicorp's "Gigabugs" benchmark (which evaluates queries across a dataset of 1.2 million rows), the client syncs and indexes only active views and recently viewed records. This keeps UI interactions sub-second while maintaining reactive local updates.

Shifting authorization to the replication layer

In a traditional REST architecture, authorization happens per request at the API layer:

// Traditional: Auth validated at the API layer
app.get('/api/invoices/:id', authenticateUser, async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
  if (invoice.organizationId !== req.user.orgId) {
    return res.status(403).send('Forbidden');
  }
  return res.json(invoice);
});

Because a local-first client runs SQL or query-builder expressions directly against its local tables, unauthorized records must never reach the browser in the first place.

This requires moving authorization directly to the replication stream. Modern sync protocols evaluate database Row-Level Security (RLS) policies or declarative rules before streaming rows to the client (for example, through PowerSync sync rules or Zero permission manifests):

  • The client opens a sync stream with authenticated session credentials, such as a verified JWT.
  • The replication engine evaluates RLS policies or sync rules before transmitting records.
  • Only authorized rows reach the client database, keeping unauthorized data off the device entirely.

Schema evolution and multi-version drift

In standard web apps, front-end updates and database migrations deploy in lockstep, and clients adopt the new schema on reload.

In local-first systems, users may keep tabs open for days or work offline for weeks while backend migrations occur. If a client attempts to sync mutations based on an outdated schema to a modified backend, naive replication will fail.

Handling schema drift reliably requires three patterns:

1. Additive server migrations

Authoritative central databases maintain backwards compatibility. Columns are not renamed or dropped without a phased deprecation period.

2. Client-side migration management

On startup, the local SQLite or PGlite instance runs local migration scripts in sequence before starting the sync engine.

3. Bidirectional transformation via schema lenses

For multi-version drift, architectures draw on research from Ink & Switch's Project Cambria. Schema lenses define bidirectional transformations between schema versions:

Server Schema v5 <---> [Lens v4-v5] <---> [Lens v3-v4] <---> Client Schema v3

These lenses translate read and write operations between older client models (like Schema v3) and the backend model (like Schema v5) without corrupting concurrent data.

Implementing local-first systems

Moving to a local-first architecture fundamentally shifts how applications handle state, replication, and data governance. It delivers instantaneous response times and keeps critical workflows operational regardless of network volatility.

At Lidex, we design and modernize enterprise software where network disruptions cannot compromise operations—leveraging WASM runtimes, OPFS persistence, and server-reconciled sync engines to build reliable, high-throughput applications.