Apache CouchDB: Installation, Multi-Directional Replication, and Real-World Offline-First Sync

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Get CouchDB Running in 5 Minutes — Do First, Ask Questions Later

I won’t start with theory. Let’s get it installed, run it, and then you’ll understand why it’s different.

Installing on Ubuntu/Debian

# Add the official Apache CouchDB GPG key and repository
curl -L https://couchdb.apache.org/repo/keys.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/couchdb.gpg
echo "deb https://apache.jfrog.io/artifactory/couchdb-deb/ $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/couchdb.list

sudo apt update && sudo apt install -y couchdb

The setup wizard will ask for a mode: choose standalone for a single node, or clustered for multiple machines. Set your admin password here — you won’t get another chance.

Verify the service is up:

sudo systemctl status couchdb
curl http://localhost:5984/
# Expected output:
# {"couchdb":"Welcome","version":"3.x.x", ...}

Creating Your First Database and Document

# Create a database named "myapp"
curl -X PUT http://admin:yourpassword@localhost:5984/myapp

# Add the first JSON document
curl -X POST http://admin:yourpassword@localhost:5984/myapp \
  -H "Content-Type: application/json" \
  -d '{"name": "Nguyen Van A", "role": "developer", "city": "Hanoi"}'

# View all documents in the database
curl http://admin:yourpassword@localhost:5984/myapp/_all_docs?include_docs=true

Visit http://localhost:5984/_utils to access Fauxton — the built-in Web UI, no extra installation needed. Creating, viewing, and editing documents manually here is fast and convenient during development.


I’ve Used MySQL, PostgreSQL, and MongoDB — What Problem Does CouchDB Actually Solve?

Having worked with all three across multiple projects, I’ve found each has its place. MySQL/PostgreSQL excel when data has a fixed structure and clear relationships. MongoDB offers more schema flexibility, but its replication leans toward primary-secondary — secondary nodes are read-only, not writable.

CouchDB solves a fundamentally different problem: apps that need to work without a network connection, then sync back up when connectivity returns. Field worker apps, POS systems in low-signal areas, logistics systems in remote locations — this is CouchDB’s home turf.

Three Ways CouchDB Is Genuinely Different

  • Pure HTTP API: Every operation goes through REST. No special driver needed — curl is all you need to get started.
  • MVCC (Multi-Version Concurrency Control): No write locks. Each document has a _rev field tracking its revision. Reads are never blocked by writes, giving you excellent read throughput.
  • Multi-master replication: Every node accepts writes. When syncing, CouchDB automatically detects conflicts — you decide how to resolve them, not the database.

CouchDB’s Document Model

Each document is JSON with an _id (auto-generated or custom) and a _rev (revision hash):

{
  "_id": "order_20260710_001",
  "_rev": "1-abc123def456",
  "customer": "Tran Thi B",
  "items": [
    {"sku": "LAP001", "qty": 2, "price": 15000000},
    {"sku": "MOU002", "qty": 1, "price": 350000}
  ],
  "total": 30350000,
  "status": "pending",
  "created_at": "2026-07-10T09:00:00Z"
}

Every update changes the _rev. If two nodes edit the same document while offline, CouchDB keeps both versions and marks the document as conflicted — you decide how to merge later.


Configuring Replication — Where CouchDB Truly Shines

One-Way Replication

# Replicate the "myapp" database from server A to server B
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{
    "source": "http://admin:pass@server-a:5984/myapp",
    "target": "http://admin:pass@server-b:5984/myapp",
    "create_target": true
  }'

Continuous Replication

# "continuous": true keeps replication running indefinitely, not just once
curl -X POST http://admin:pass@localhost:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{
    "source": "http://admin:pass@server-a:5984/myapp",
    "target": "http://admin:pass@server-b:5984/myapp",
    "continuous": true,
    "create_target": true
  }'

Bidirectional Replication

Run two jobs in parallel — one direction A→B, one direction B→A:

# Direction A → B
curl -X POST http://admin:pass@server-a:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{"source": "myapp", "target": "http://admin:pass@server-b:5984/myapp", "continuous": true}'

# Direction B → A
curl -X POST http://admin:pass@server-b:5984/_replicate \
  -H "Content-Type: application/json" \
  -d '{"source": "myapp", "target": "http://admin:pass@server-a:5984/myapp", "continuous": true}'

Once configured, both servers sync automatically whenever changes occur — even when one side has just come back online after a period of downtime. No additional setup needed.


Handling Conflicts After Sync

Conflicts happen more often than you’d expect — especially when field workers use the app on spotty 3G connections, go offline for a couple of hours, then sync back. CouchDB doesn’t auto-merge content — it lets you choose which revision wins.

import requests

base = "http://admin:yourpassword@localhost:5984"

# Find documents with conflicts
def find_conflicts(db):
    url = f"{base}/{db}/_all_docs?include_docs=true&conflicts=true"
    docs = requests.get(url).json()["rows"]
    return [row["doc"] for row in docs if "_conflicts" in row.get("doc", {})]

# Get the conflicting revisions
def get_conflict_revs(db, doc_id, conflict_revs):
    results = []
    for rev in conflict_revs:
        url = f"{base}/{db}/{doc_id}?rev={rev}"
        results.append(requests.get(url).json())
    return results

# Delete the losing revision (keep the one you want)
def resolve_conflict(db, doc_id, losing_rev):
    url = f"{base}/{db}/{doc_id}?rev={losing_rev}"
    resp = requests.delete(url)
    print(f"Deleted losing rev: {resp.status_code}")

# Example usage
conflicts = find_conflicts("myapp")
for doc in conflicts:
    print(f"Conflict on: {doc['_id']}")
    revs = get_conflict_revs("myapp", doc["_id"], doc["_conflicts"])
    # Keep the current revision (winning), delete the conflicting ones
    for rev in doc["_conflicts"]:
        resolve_conflict("myapp", doc["_id"], rev)

Merge logic depends on your business rules: for orders you might take the latest by created_at, for activity logs you might want to keep both. CouchDB provides the mechanism — you write the rules.


4 Things to Know Before Using CouchDB in Production

1. Pair It with PouchDB on the Client — a Complete Offline-First Stack

PouchDB is a JavaScript library (~46KB gzipped) that runs directly in the browser or React Native. Its API mirrors CouchDB’s, and it automatically syncs both ways when connectivity is restored:

// In your web/React Native app
const db = new PouchDB('local_myapp');
const remoteDB = new PouchDB('http://admin:pass@yourserver:5984/myapp');

// Enable bidirectional sync, auto-reconnects when network is restored
db.sync(remoteDB, { live: true, retry: true })
  .on('change', info => console.log('Synced:', info))
  .on('error', err => console.error('Sync error:', err));

Users can enter data offline without any disruption. When the network returns, PouchDB automatically pushes and pulls changes — no extra code required.

2. Create Indexes for Faster Queries

By default, CouchDB uses views (MapReduce) — verbose to write. Since CouchDB 2.x, Mango Query is available and much easier:

# Create an index on the "status" and "created_at" fields
curl -X POST http://admin:pass@localhost:5984/myapp/_index \
  -H "Content-Type: application/json" \
  -d '{"index": {"fields": ["status", "created_at"]}, "name": "status-index"}'

# Query using Mango syntax
curl -X POST http://admin:pass@localhost:5984/myapp/_find \
  -H "Content-Type: application/json" \
  -d '{
    "selector": {"status": "pending"},
    "sort": [{"created_at": "desc"}],
    "limit": 20
  }'

3. When NOT to Use CouchDB

  • Need complex JOINs across multiple tables → use PostgreSQL.
  • Time-series data, metrics → use TimescaleDB or InfluxDB.
  • High-speed caching → use Redis.
  • CouchDB is the right choice when: data is document-shaped, you need offline-first functionality, and you want multi-master without dealing with distributed locks.

4. Enable CORS When Using PouchDB from the Browser

curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/httpd/enable_cors \
  -d '"true"'
curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/cors/origins \
  -d '"*"'
curl -X PUT http://admin:pass@localhost:5984/_node/nonode@nohost/_config/cors/methods \
  -d '"GET,PUT,POST,HEAD,DELETE"'

Replace "*" with your specific domain in production. Using "*" during development is fine, but don’t leave it that way when you deploy.

CouchDB isn’t the right database for every problem. But when your app needs to run without a network and sync back, or when multiple nodes need to write simultaneously without the headache of distributed locking — it handles exactly that, no workarounds needed.

Share: