pouchdb-relay
v0.1.0
Published
Multiplexed CouchDB _changes feed over WebSocket for PouchDB replication
Maintainers
Readme
pouchdb-relay
Multiplexed CouchDB _changes feed over WebSocket for PouchDB replication.
Problem
CouchDB filtered replication opens one continuous _changes feed per user. Each feed scans the entire changes sequence, making per-user sync expensive at scale. With prefix-based filtering (username-book:*), every connected user creates a separate server-side feed that walks the same global sequence.
Solution
pouchdb-relay multiplexes the _changes feed:
- Server maintains ONE continuous
_changesfeed on the database - Clients connect via WebSocket and subscribe with their prefix
- Server fans out matching changes to each client in real-time
- All other replication calls (
_revs_diff,_bulk_docs,_bulk_get,_local) pass directly to CouchDB
This reduces server load from O(users) change feeds to O(1).
Client Usage
import { createRelayFetch } from 'pouchdb-relay/client';
const relayFetch = createRelayFetch({
relayUrl: 'wss://services.example.com/relay',
originalFetch: PouchDB.fetch,
getHeaders: async () => ({ Authorization: `Bearer ${token}` }),
prefix: 'username',
});
const remoteDb = new PouchDB('https://couch.example.com/activities', {
fetch: relayFetch,
});
// PouchDB replication works normally — _changes goes through
// the relay, everything else goes direct to CouchDB
remoteDb.sync(localDb, { live: true, retry: true });
// Clean up when done
relayFetch.destroy();How it works
createRelayFetchreturns afetchwrapper function- When PouchDB calls
fetch(url, opts):- If URL contains
/_changes→ route through WebSocket relay - Otherwise → call
originalFetch(url, opts)directly to CouchDB
- If URL contains
- For intercepted
_changesrequests:- Parse query params from the URL (
since,heartbeat,feed,filter,selector) - Open/reuse a WebSocket connection to
relayUrl - Send a
subscribemessage with the prefix andsincevalue - Return a synthetic
Responsewith aReadableStreambody that emits change events
- Parse query params from the URL (
- WebSocket lifecycle:
- Auto-reconnect with exponential backoff (1s → 30s max)
- Re-subscribe on reconnect with last known
since - Ping/pong heartbeat to detect dead connections (25s interval, 10s timeout)
- Clean close when PouchDB cancels replication (abort signal)
Options
interface RelayOptions {
relayUrl: string; // WebSocket URL of the relay server
originalFetch: typeof fetch; // PouchDB's fetch (for non-_changes requests)
getHeaders: () => Promise<Record<string, string>>; // Auth headers
prefix: string; // User's doc prefix for filtering
reconnectBaseDelay?: number; // Base delay in ms (default: 1000)
reconnectMaxDelay?: number; // Max delay in ms (default: 30000)
heartbeatInterval?: number; // Ping interval in ms (default: 25000)
heartbeatTimeout?: number; // Pong timeout in ms (default: 10000)
}Server Protocol
The server is not included in this package. Below is the protocol specification for implementing the relay server.
WebSocket Messages
Client → Server:
// Subscribe to changes matching a prefix
{ "type": "subscribe", "prefix": "username", "since": "123-abc", "database": "activities" }
// Unsubscribe from changes
{ "type": "unsubscribe" }
// Heartbeat ping
{ "type": "ping" }Server → Client:
// A matching change
{
"type": "change",
"seq": "124-def",
"id": "username-book:abc",
"changes": [{ "rev": "1-xyz" }],
"deleted": false
}
// Periodic checkpoint (seq update without matching changes)
{ "type": "checkpoint", "seq": "200-ghi" }
// Heartbeat response
{ "type": "pong" }
// Error
{ "type": "error", "message": "Authentication failed" }Server Responsibilities
- Single feed: Maintain ONE continuous
_changesfeed on the target database - Fan-out: For each connected client, filter changes where
doc._id.startsWith(prefix + '-') - Immediate delivery: Send matching changes as they arrive
- Checkpoints: Send periodic
checkpointmessages so clients can tracksinceduring quiet periods - Authentication: Validate JWT from WebSocket connection params (passed as query string by the client)
- Prefix validation: Ensure the authenticated user matches the requested prefix
Server Implementation Notes
Single CouchDB _changes feed (continuous)
│
▼
┌─────────────┐
│ Relay Server │
└─────┬───────┘
│
┌────┼────┐
▼ ▼ ▼
WS1 WS2 WS3 (one per connected client)
user1 user2 user3The relay maintains a map of prefix → Set<WebSocket>. On each change from the CouchDB feed:
- Extract the prefix from
change.id(everything before the first-) - Look up connected clients for that prefix
- Send the change to matching clients
- Periodically (e.g., every 10s or every 100 changes), send a
checkpointto all clients with the currentseq
