Replicache: Offline-First Sync Engine
AI TOOLS Feb. 12, 2026, 5:30 p.m.

Replicache: Offline-First Sync Engine

Replicache is a modern JavaScript library that puts offline‑first at the heart of web app development. It lets you write data‑centric code once, then automatically handles local persistence, conflict resolution, and sync with a server when the network returns. In this article we’ll unpack how Replicache works under the hood, walk through a full implementation, and explore real‑world scenarios where its offline‑first approach shines.

Core Concepts

At its core Replicache maintains two separate data stores: a client store that lives in IndexedDB and a server store that you host (often a simple key‑value database). The client store is the source of truth while the user is offline; every mutation you write is queued locally and later replayed against the server.

Mutations are pure functions that receive the current client state and return a new state. Because they’re deterministic, Replicache can safely re‑apply them on the server to achieve eventual consistency. This functional style also makes conflict resolution predictable: the server runs the same mutation logic, and any divergence is resolved by the same code path.

Version Vectors and Sync Tokens

Replicache uses a lightweight version vector to track which mutations have been applied on each side. When the client reconnects, it sends a sync token that tells the server “I have applied mutations up to version X”. The server then streams back any newer mutations it has, allowing both sides to converge without sending the entire data set.

This approach keeps network payloads tiny, even for apps with thousands of records. It also means you can build rich, collaborative experiences without a heavyweight operational transformation engine.

Getting Started

First, add Replicache to your project via npm or yarn. The library itself is only a few kilobytes, but it pulls in a small IndexedDB wrapper for persistence.

npm install replicache

Next, initialize a Replicache instance in your front‑end code. The pushURL points to your server’s endpoint that will receive mutation batches, while pullURL fetches new mutations from the server.

import { Replicache } from 'replicache';

const rep = new Replicache({
  name: 'my-app',
  pushURL: '/api/replicache/push',
  pullURL: '/api/replicache/pull',
});

That’s all you need to get a functional offline store up and running. The rest of the article shows how to define mutations, handle conflicts, and integrate with a real back‑end.

Defining Mutations

Mutations are declared as async functions that receive a tx (transaction) object. Use tx.put, tx.del, and tx.get to manipulate the client store. Here’s a simple “add note” mutation for a note‑taking app.

rep.registerMutators({
  async addNote({ id, title, content }, { mutators, tx }) {
    const timestamp = Date.now();
    await tx.put(`note:${id}`, { id, title, content, timestamp });
    return { success: true };
  },
});

The same mutation runs on the server, so you must provide an identical implementation there. This guarantees that the server’s view of the data matches the client’s after the mutation is replayed.

Server‑Side Mutator Example (Python)

Below is a minimal FastAPI endpoint that receives mutation batches and applies them using the same pure functions. The server stores data in an in‑memory dictionary for demo purposes.

from fastapi import FastAPI, Request
from typing import Dict, Any

app = FastAPI()
db: Dict[str, Any] = {}

async def add_note_mutator(payload: dict):
    note_id = payload['id']
    db[f'note:{note_id}'] = {
        'id': note_id,
        'title': payload['title'],
        'content': payload['content'],
        'timestamp': payload['timestamp']
    }

@app.post("/api/replicache/push")
async def push(request: Request):
    body = await request.json()
    for mutation in body['mutations']:
        if mutation['name'] == 'addNote':
            await add_note_mutator(mutation['args'])
    return {"status": "ok"}

In production you’d replace the in‑memory dict with a persistent store (PostgreSQL, DynamoDB, etc.) and add proper authentication.

Pulling New Data

The client periodically calls the pullURL to fetch any mutations it missed while offline. The server responds with a JSON payload containing a list of mutations and an updated sync token.

@app.post("/api/replicache/pull")
async def pull(request: Request):
    body = await request.json()
    client_token = body.get('clientToken')
    # For demo, send all notes after the token (none in this simple case)
    mutations = []  # Populate with real mutations in a real app
    server_token = "v2"  # Incremented version after processing
    return {"mutations": mutations, "serverToken": server_token}

When the client receives this response, Replicache automatically applies the mutations to the local store, updates its version vector, and notifies any listeners you’ve attached.

Listening to Changes

React developers love the useSubscribe hook that Replicache provides. It lets components re‑render only when the specific data they care about changes, keeping UI updates fast and deterministic.

import { useSubscribe } from 'replicache-react';

function NoteList() {
  const notes = useSubscribe(rep, async tx => {
    const keys = await tx.scan({ prefix: 'note:' });
    const results = [];
    for await (const { key } of keys) {
      results.push(await tx.get(key));
    }
    return results;
  }, []);
  
  return (
    <ul>
      {notes.map(note => (
        <li key={note.id}>{note.title}</li>
      ))}
    </ul>
  );
}

Because the subscription runs inside a transaction, it always sees a consistent snapshot of the client store, even if a sync is happening in the background.

Handling Conflicts

Conflicts arise when two clients edit the same record while offline. Replicache’s deterministic mutation model means the server will apply mutations in the order they were received, but you can embed custom conflict‑resolution logic inside the mutator.

Consider a collaborative counter where each client can increment a value. The mutator simply adds the delta; because addition is commutative, order doesn’t matter and the final count is correct.

rep.registerMutators({
  async incCounter({ delta }, { tx }) {
    const current = (await tx.get('counter'))?.value || 0;
    await tx.put('counter', { value: current + delta });
  },
});

For non‑commutative operations (e.g., editing a text field), you might store a last‑write‑wins timestamp or merge changes using a CRDT library. The key is to keep the resolution logic pure and side‑effect‑free.

Real‑World Use Cases

1. Field Data Collection – Surveyors often work in remote areas with spotty connectivity. Using Replicache, each device stores responses locally, queues mutations, and syncs automatically when a cellular connection appears. No custom offline queue is needed.

2. Collaborative Note‑Taking – Apps like Notion or Evernote benefit from instant UI updates even when offline. Replicache ensures that edits appear instantly, then merges them on the server without losing any changes.

3. E‑Commerce Cart Persistence – Users can add items to a cart while offline; the cart state lives in IndexedDB and syncs to the back‑end as soon as the user goes online, guaranteeing a seamless checkout experience.

Performance Tips

Replicache is designed to be lightweight, but a few best practices keep it snappy:

  • Batch Mutations – Group related changes into a single mutation to reduce round‑trips.
  • Use Prefix Scans – When reading many records, scan by prefix instead of fetching each key individually.
  • Limit Mutation Size – Keep mutation payloads under a few kilobytes; large blobs should be stored elsewhere (e.g., S3) and referenced by ID.

Pro Tip: Enable Replicache’s debug flag during development. It logs every push/pull cycle, helping you spot unnecessary network traffic before it reaches production.

Testing Offline Scenarios

Automated testing of offline behavior is essential. Use the replicache-test package to simulate network loss, inject mutations, and verify that the client store reaches the expected state after a simulated reconnection.

import { makeTestReplicache } from 'replicache-test';

async function testOfflineSync() {
  const { rep, push, pull } = await makeTestReplicache();
  await rep.mutate.addNote({ id: '1', title: 'Draft', content: '' });
  // Simulate offline: push does nothing
  await push({ succeed: false });
  // Simulate reconnection
  await push({ succeed: true });
  const note = await rep.query(async tx => await tx.get('note:1'));
  console.assert(note.title === 'Draft');
}
testOfflineSync();

Running such tests in CI ensures that future changes don’t break the offline‑first guarantees.

Security Considerations

Because mutations travel over the network, they must be authenticated and authorized. A common pattern is to embed a JWT in the Authorization header of both push and pull requests. The server validates the token before applying any mutation.

Additionally, validate mutation payloads server‑side. Never trust client‑provided timestamps or IDs; generate server‑side identifiers when possible to avoid injection attacks.

Scaling Replicache

When your user base grows, the server’s mutation queue can become a bottleneck. Sharding by user ID or tenant, and using a durable message queue (Kafka, SQS) to process pushes asynchronously, helps maintain low latency.

For read‑heavy workloads, consider materializing the Replicache state into a read‑optimized store (e.g., Elasticsearch) and serving queries directly from there, while the mutation processor updates the source of truth.

Pro Tip: Store the latest sync token in a Redis cache per user. This allows the pull endpoint to quickly compute “what’s new since X?” without scanning the entire mutation log.

Advanced Patterns

CRDT Integration – If your domain requires complex concurrent edits (e.g., rich‑text documents), you can embed a CRDT library inside a mutation. The mutation receives the current CRDT state, applies the remote operation, and writes back the merged state.

Optimistic UI with Rollback – Replicache already provides optimistic updates, but you can enhance the experience by storing a snapshot before a mutation. If the server later rejects the mutation (e.g., due to validation), roll back to the snapshot and display an error.

Monitoring & Observability

Instrument both client and server with metrics: number of pushes per minute, average payload size, sync latency, and conflict rate. Tools like Prometheus (server) and the browser’s Performance API (client) give you visibility into the sync pipeline.

Alert on spikes in conflict rate – a sudden increase often signals a bug in mutation ordering or an unexpected offline pattern.

Conclusion

Replicache abstracts away the gritty details of offline persistence, conflict resolution, and incremental sync, letting developers focus on business logic. By embracing its functional mutation model, you gain deterministic behavior, tiny network payloads, and a seamless user experience that works even when the internet doesn’t. Whether you’re building a field‑data collector, a collaborative editor, or a resilient e‑commerce cart, Replicache provides a solid foundation for offline‑first web apps. Start experimenting today, and let your users stay productive—no matter where they are.

Share this article