Build an Offline Survey Sync Workflow with CouchDB
A field survey app cannot assume that every device has a stable connection. A worker may collect data for hours, reconnect later, and discover that someone at the office edited the same record.
CouchDB is useful here because replication is part of the database rather than an external synchronization service. The hard part is conflict handling: CouchDB preserves conflicting revisions, but your application still has to decide how to merge them.
Who this is for: TypeScript developers evaluating CouchDB for offline or multi-site data.
Outcome: Two databases that represent a field device and a central server, with replication and a real concurrent-edit conflict.
Time: About 20 minutes, plus the first CouchDB binary download.
Prerequisites: Node.js 20 or newer and pnpm. No Docker or Cloud account is required.
Start CouchDB locally
Install the Layerbase CLI, then create a local CouchDB instance:
npm i -g layerbase
lbase create couch-surveys -e couchdb --start
lbase url couch-surveysThe last command prints the actual URL:
http://127.0.0.1:5984/couch_surveysIf port 5984 is busy, the CLI chooses another port. Copy the URL it prints rather than assuming the default.
The final path is the default database created with the instance. This tutorial creates two other databases, so the script reduces that URL to the server origin before connecting.
Local CouchDB uses admin as both the default username and password. The script below adds those credentials when COUCH_URL does not already contain credentials.
Create the project:
mkdir couchdb-survey-sync
cd couchdb-survey-sync
pnpm init
pnpm add nano
pnpm add -D tsx typescript @types/nodeBuild the sync example
Create survey-sync.ts:
import nano from 'nano'
type Survey = {
_id: string
_rev?: string
_conflicts?: string[]
surveyor: string
location: string
responses: {
satisfaction: number
notes: string
}
reviewed?: boolean
}
const connectionUrl = new URL(
process.env.COUCH_URL ?? 'http://127.0.0.1:5984',
)
if (!connectionUrl.username) {
connectionUrl.username = 'admin'
connectionUrl.password = 'admin'
}
connectionUrl.pathname = '/'
connectionUrl.search = ''
connectionUrl.hash = ''
const couch = nano(connectionUrl.toString())
const DEVICE_DB = 'survey_device_tutorial'
const SERVER_DB = 'survey_server_tutorial'
async function recreateDatabase(name: string): Promise<void> {
const databases = await couch.db.list()
if (databases.includes(name)) {
await couch.db.destroy(name)
}
await couch.db.create(name)
}
await recreateDatabase(DEVICE_DB)
await recreateDatabase(SERVER_DB)
const device = couch.use<Survey>(DEVICE_DB)
const server = couch.use<Survey>(SERVER_DB)
const surveys: Survey[] = [
{
_id: 'survey-001',
surveyor: 'Alice',
location: 'North Ridge',
responses: {
satisfaction: 4,
notes: 'Trail is open after overnight rain.',
},
},
{
_id: 'survey-002',
surveyor: 'Ben',
location: 'River Delta',
responses: {
satisfaction: 3,
notes: 'Water level is above the seasonal average.',
},
},
{
_id: 'survey-003',
surveyor: 'Alice',
location: 'Coastal Bluff',
responses: {
satisfaction: 5,
notes: 'No erosion near the marked observation point.',
},
},
]
await device.bulk({ docs: surveys })
console.log(`Saved ${surveys.length} surveys on the field device`)
await couch.db.replicate(DEVICE_DB, SERVER_DB)
const firstSync = await server.list()
console.log(`Central server now has ${firstSync.rows.length} surveys`)
// Both sides read the same revision before going offline.
const deviceCopy = await device.get('survey-001')
const serverCopy = await server.get('survey-001')
// The field worker changes the notes.
await device.insert({
...deviceCopy,
responses: {
...deviceCopy.responses,
notes: 'Trail reopened, but the lower bridge is still slippery.',
},
})
// An office reviewer changes a different field on the same document.
await server.insert({
...serverCopy,
reviewed: true,
})
await couch.db.replicate(DEVICE_DB, SERVER_DB)
const afterSync = await server.get('survey-001', { conflicts: true })
console.log(`Visible revision: ${afterSync._rev}`)
console.log(`Hidden conflicting revisions: ${afterSync._conflicts?.length ?? 0}`)The script deliberately destroys and recreates only the two databases whose names end in _tutorial. Do not reuse those names for real data.
Run it with the URL returned by the CLI:
COUCH_URL="$(lbase url couch-surveys)" pnpm tsx survey-sync.tsExpected output:
Saved 3 surveys on the field device
Central server now has 3 surveys
Visible revision: 2-<revision hash>
Hidden conflicting revisions: 1The revision hash will differ on every run. The important result is the final 1: both edits reached the central database, and CouchDB recorded a conflict.
What CouchDB did, and what it did not do
On a single database, an update must include the current _rev. A stale update is rejected with HTTP 409, which prevents an unnoticed overwrite on that node.
Replication is different. When two databases accept updates from the same starting revision, both revisions are copied. CouchDB chooses one revision as the visible winner using a deterministic algorithm. It does not understand that one person changed notes while another changed reviewed, and it does not merge those fields for you.
A production application should:
- Read documents with
conflicts: true. - Fetch each revision listed in
_conflicts. - Apply a domain-specific merge policy.
- Write the merged document as a new revision.
- Delete the losing revisions after the merge is verified.
Some data can be merged automatically. A set of tags is a good example. Free-form notes or a status transition may need a person to choose the correct value.
Do not use _rev as an audit log. The CouchDB document design guide warns that compaction can remove old revision bodies. If the business needs history, store immutable audit events or explicit document snapshots.
Replication is not a backup
The two databases in this tutorial are replicas. If your application deletes a survey and replication runs, that deletion can reach the other database. A bad update can replicate just as easily as a good one.
A backup needs a recovery point that is independent of the live replication stream. For a production system:
- Keep scheduled backups or storage snapshots with a defined retention period.
- Test restoring into a separate database.
- Record document counts and sample checksums before a migration or cutover.
- Keep the source database available until the restored target passes verification.
For a Cloudant move, the Cloudant to CouchDB migration guide includes cutover, verification, and rollback steps.
Run the same workflow on Layerbase Cloud
Once the local workflow is proven, create a CouchDB database on Layerbase Cloud and copy the generated HTTPS URL from Quick Connect into COUCH_URL. Do not construct the hostname or port yourself.
CouchDB requires the Pro plan. As verified on July 23, 2026, Pro is $15 per month, supports up to 10 databases, and includes daily backups with 30-day retention. Check the current pricing page before making a purchasing decision because plan details can change.
Cloud is a better fit when you want a managed central CouchDB endpoint with TLS and scheduled backups. It is not a replacement for conflict policy, restore testing, or device-side storage. PouchDB is usually a better device or browser companion than running a full CouchDB server on every client.
When CouchDB is the wrong fit
CouchDB is a strong candidate when disconnected writers and replication are central requirements. It is usually a poor fit when:
- The application needs relational joins and multi-row constraints.
- Most work is ad-hoc analytics across many fields.
- There is only one central service and ordinary CRUD is the whole problem.
- The team does not have a clear conflict-resolution policy.
For local development, the Layerbase CLI is the shortest path. Layerbase Desktop provides the same local engine workflow in a GUI on macOS, Windows, and Linux. Move to Cloud after the sync and recovery behavior is understood, not before.
Keep reading
- IBM Cloudant deprecations in 2025 and 2026: database caps, QuickJS, and removed featuresA complete, dated rundown of what IBM Cloudant has changed, deprecated, or removed in the last 18 months, including the new 20-database Lite-plan cap, the SpiderMonkey to QuickJS cutover on October 6, 2026, and the removed CouchApp design-doc handlers. With migration paths if any of these break your app.
- Serverless CouchDB in 2026: scale-to-zero managed Apache CouchDBWhat "serverless CouchDB" actually means in 2026, why the category is small (one long-running hosted option, IBM Cloudant, plus flat-rate managed CouchDB on Layerbase Cloud), and how to ship an offline-first or document-store app without managing the database yourself.
- Cloudant alternatives: managed CouchDB hosting in 2026IBM Cloudant has been the default managed CouchDB for over a decade, and the Lite free tier has quietly gotten less generous. Here are the modern managed CouchDB hosting options, including a flat-rate managed CouchDB, serverless scale-to-zero, and one-step migration off Cloudant.
- IBM Cloudant vs Layerbase Cloud: managed CouchDB compared in 2026A direct head-to-head between IBM Cloudant and Layerbase Cloud for managed Apache CouchDB hosting. Compares entry pricing, database-count caps, the billing model, JavaScript engine, migration path, region coverage, and operational ergonomics, with a recommendation for each common workload.