Core Features

Replication and PouchDB (Offline-First)

Understand CouchDB replication and how PouchDB enables offline-first applications that sync when connectivity returns.

CouchDB Replication

CouchDB's replication is its killer feature. You can replicate:

  • CouchDB to CouchDB (bi-directional)
  • CouchDB to PouchDB (browser or mobile)
  • Continuously (real-time sync) or one-shot
bash
# Replicate from source to target (one-time)
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{
    "source": "http://admin:pass@db1:5984/myapp",
    "target": "http://admin:pass@db2:5984/myapp"
  }'

# Continuous replication
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -d '{
    "source": "myapp",
    "target": "http://remote:5984/myapp",
    "continuous": true
  }'

PouchDB: The Browser Database

PouchDB is a JavaScript database that runs in the browser (using IndexedDB) and is fully compatible with CouchDB's replication protocol. This enables the offline-first architecture:

  1. User's app writes to local PouchDB in the browser
  2. App works fully offline
  3. When connectivity returns, PouchDB syncs with CouchDB automatically
  4. Conflicts are detected and can be resolved

This pattern is extremely valuable for:

  • Mobile web apps in areas with unreliable connectivity
  • Field data collection applications
  • Collaborative tools that need to work offline
javascript
import PouchDB from 'pouchdb';

// Local database in the browser
const localDb = new PouchDB('myapp');

// Write locally (works offline)
await localDb.put({
  _id: 'note-001',
  title: 'Meeting notes',
  content: 'Discussed Q1 planning...',
  createdAt: new Date().toISOString(),
});

// Sync with remote CouchDB (when online)
const remoteDb = new PouchDB('https://your-couch.example.com/myapp');

// One-time sync
await localDb.sync(remoteDb);

// Continuous live sync
localDb.sync(remoteDb, {
  live: true,
  retry: true,    // Retry on network failure
}).on('change', (change) => {
  console.log('Synced:', change);
}).on('error', (err) => {
  console.error('Sync error:', err);
});

Handling Conflicts

When the same document is modified in two places before syncing, CouchDB creates a conflict. Unlike most databases that silently overwrite, CouchDB preserves both versions and marks the document as conflicted. Your application handles the resolution.

javascript
// Detect and resolve conflicts
async function resolveConflicts(db, docId) {
  const doc = await db.get(docId, { conflicts: true });

  if (!doc._conflicts || doc._conflicts.length === 0) return doc;

  // Fetch all conflicting revisions
  const conflictingRevs = await Promise.all(
    doc._conflicts.map(rev => db.get(docId, { rev }))
  );

  // Simple resolution: keep the most recently modified version
  const winner = [doc, ...conflictingRevs].sort(
    (a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)
  )[0];

  // Delete the losing revisions
  for (const loser of conflictingRevs) {
    if (loser._rev !== winner._rev) {
      await db.remove(loser._id, loser._rev);
    }
  }

  return winner;
}

Mango Query (Find API)

CouchDB 2+ supports Mango Query — a MongoDB-inspired JSON query language that's more approachable than MapReduce for simple queries:

javascript
// Create an index first
await db.createIndex({ index: { fields: ['role', 'createdAt'] } });

// Query
const result = await db.find({
  selector: {
    role: 'admin',
    createdAt: { $gt: '2025-01-01' },
  },
  sort: [{ createdAt: 'desc' }],
  limit: 20,
});

console.log(result.docs);

Example

javascript
// Offline-first notes app with PouchDB + CouchDB sync
import PouchDB from 'pouchdb';
import PouchFind from 'pouchdb-find';
PouchDB.plugin(PouchFind);

const local = new PouchDB('notes');
const remote = new PouchDB('https://couch.myapp.com/notes', {
  auth: { username: 'user', password: 'pass' }
});

// Write note (works offline)
async function saveNote(note) {
  return local.put({
    _id: `note:${Date.now()}`,
    ...note,
    updatedAt: new Date().toISOString(),
  });
}

// Start live sync in background
local.sync(remote, { live: true, retry: true });

// Query local notes
const { docs } = await local.find({
  selector: { type: 'note' },
  sort: [{ updatedAt: 'desc' }],
});

Want to run this code interactively?

Try in Compiler