Back to TechSheets
FrontendArchitectureLocal-FirstCRDTSignalsPerformance

Beyond State Managers: Architecting Local-First UIs with CRDTs and Reactive Signals

Thanga MariappanSenior Architect
6 min read
Mar 23, 2026
Beyond State Managers: Architecting Local-First UIs with CRDTs and Reactive Signals

Beyond State Managers: Architecting Local-First UIs with CRDTs and Reactive Signals

For the last decade, frontend development has been dominated by a single paradigm: the "Thin Client." In this model, the browser acts as a temporary cache for data that lives on a remote server. We spend a disproportionate amount of our engineering time managing loading states, handling partial network failures, and fighting with useEffect or TanStack Query to keep the UI in sync with a distant database.

But the wind is shifting. We are entering the era of Local-First Development.

In a local-first architecture, the primary source of truth is not the server—it is a database living inside the user's browser. The UI interacts with this local store at 0ms latency, and synchronization with the cloud happens as a background process. This article explores how to architect these hyper-resilient systems using Conflict-free Replicated Data Types (CRDTs) and the latest reactive primitives.

The Architecture of a Local-First Frontend

Traditional applications follow a Request-Response cycle. Local-first applications follow a Sync-and-Observe cycle. This requires a fundamental shift in the stack:

  1. Storage Layer: IndexedDB or SQLite (via WASM) instead of transient memory.
  2. Sync Engine: A background protocol (WebSockets, WebRTC, or Server-Sent Events) that handles delta-updates between clients.
  3. Conflict Resolution: Using CRDTs to ensure that two users editing the same field eventually reach the same state without a central coordinator.
  4. Reactivity Layer: Fine-grained signals that bind UI components directly to database queries.

The Deep Dive: How CRDTs Solve the Multi-User Problem

One of the biggest hurdles in frontend architecture is conflict resolution. When two users edit the same document offline and then reconnect, whose change wins? Traditional "Last Write Wins" (LWW) strategies are destructive and lead to data loss.

CRDTs (Conflict-free Replicated Data Types) are mathematical structures that allow multiple replicas to be updated independently and concurrently. When these replicas exchange data, they are guaranteed to merge into the same state mathematically.

The LWW-Element-Set Example

Imagine a simple shared counter. Instead of storing an integer, we store a set of operations with timestamps. Even if updates arrive out of order, the logic inside the CRDT ensures the final state is deterministic.

In a modern stack, we don't write these from scratch. Tools like Yjs or Automerge provide high-level abstractions for arrays, maps, and text that handle the heavy lifting of CRDT merging.

Practical Implementation: RxDB + Signal-Based Frameworks

Let’s look at a practical implementation using RxDB (a NoSQL database for JavaScript) and a signal-based approach (compatible with Preact, SolidJS, or even modern React via useSyncExternalStore).

1. Defining the Persistent Schema

First, we define a schema that lives in the browser's IndexedDB. Unlike Redux, this state survives a page refresh or a browser crash automatically.

import { createRxDatabase, addRxPlugin } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';

const db = await createRxDatabase({
    name: 'techsheet_db',
    storage: getRxStorageDexie()
});

await db.addCollections({
    projects: {
        schema: {
            version: 0,
            primaryKey: 'id',
            type: 'object',
            properties: {
                id: { type: 'string', maxLength: 100 },
                name: { type: 'string' },
                status: { type: 'string' },
                lastEdited: { type: 'number' }
            },
            required: ['id', 'name']
        }
    }
});

2. The Reactive Bridge

In a local-first world, we don't "fetch" data. We "subscribe" to a query. Using a signal-based approach (like Preact Signals), we can bridge the database to the DOM with surgical precision.

import { signal, effect } from "@preact/signals";

export function useProjectList(db) {
    const projects = signal([]);

    // Subscribe to the database query
    const query = db.projects.find({
        selector: {},
        sort: [{ lastEdited: 'desc' }]
    });

    query.$.subscribe(results => {
        projects.value = results;
    });

    return projects;
}

Because the database is local, query.$.subscribe fires instantly. There is no skeleton screen, no spinner, and no "Loading..." message. The data is simply there.

Why This Beats Traditional REST/GraphQL Patterns

1. Perceived Performance

In a traditional app, when a user clicks "Save," we show a loading spinner, wait for the server, and then update the UI. In a local-first app, the "Save" action writes to the local DB. This takes ~1ms. The UI updates instantly. The sync with the server happens in the background. This is "Optimistic UI" by default, rather than as a manual optimization.

2. Multi-Tab Synergy

Because the state is persisted in IndexedDB and shared across the same origin, if a user has your app open in three different tabs, an update in Tab A instantly reflects in Tab B and C without a single network request. The database acts as a local message bus.

3. Resilience and Accessibility

Your application becomes inherently offline-capable. For users on spotty mobile connections or in regions with high latency, the app remains fully interactive. The delta-sync protocols used by local-first engines are far more efficient than re-fetching large JSON blobs over REST.

The Challenges: Security and Storage Limits

This paradigm isn't a silver bullet. It introduces two primary challenges:

  • Security (RBAC): Since the user has a copy of the database, you cannot rely on "hidden" data fields. You must filter data at the sync layer, ensuring only the data the user is authorized to see is ever sent to their local storage.
  • Storage Quotas: While IndexedDB can store gigabytes, browsers may prune data if the device runs low on space. A robust local-first architecture must handle "eviction" strategies, where old or less-frequently accessed data is moved to "cloud-only" status.

Real-World Use Case: Collaborative Engineering Tools

Consider a Figma-like design tool or a collaborative CAD platform. Using traditional Redux/REST, managing the sheer volume of updates is a nightmare. By using a local-first approach with Replicache or RxDB + Yjs, the frontend architect can treat the UI as a pure reflection of the local CRDT state. The complexity of "who changed what" is abstracted away into the mathematical merge logic of the CRDT, allowing developers to focus on the user experience rather than race conditions.

Summary

The move toward local-first is a move toward a more robust, respectful, and performant web. By shifting our mindset from "Fetching Data" to "Syncing Databases," we eliminate the most frustrating parts of frontend development: the loading state and the network error.

As tools like ElectricSQL, PowerSync, and RxDB mature, the barrier to entry for local-first is dropping. If you are starting a new high-interaction SaaS today, sticking to a traditional thin-client architecture might soon be considered technical debt.

Key Takeaways:

  • Local-first means the local DB is the source of truth.
  • CRDTs allow for seamless conflict resolution without a central server.
  • Signals provide the high-performance reactivity needed to map DB queries to the UI.
  • User Experience is drastically improved through zero-latency interactions and offline resilience.
0Likes
Share this techsheet
Share TechSheet

Discussion

0 characters