Cross-database replication in Node.js — Postgres → Mongo CDC, CQRS read replicas & failover with @mostajs/replicator

Cross-database replication in Node.js — Postgres → Mongo CDC, CQRS read replicas & failover with @mostajs/replicator

Part of the @mostajs/orm ecosystem — the multi-dialect ORM with one API across 13 databases. This post zooms in on replication.

TL;DR

@mostajs/replicator gives a Node/TypeScript app database replication as a library — no Debezium, no Kafka, no broker to operate:

npm install @mostajs/replicator @mostajs/orm @mostajs/mproject

The problem nobody packages

You have a primary database and you want: read replicas to take load off the master, a failover path when the master dies, and — increasingly — a different store for analytics or search (Mongo, a column store) fed from your transactional Postgres.

In the JS/TS world there is no library answer. Prisma and Drizzle are query builders; replication is “someone else’s problem”. So teams reach for Debezium + Kafka Connect, a managed CDC service, or a pile of bespoke cron scripts — infrastructure to stand up, secure and babysit, long before the first row moves.

@mostajs/replicator collapses that into a dependency. It sits on top of @mostajs/orm, so every replica is just an IDialect + isolated EntityService — the same schema model you already use.

Feature 1 — CQRS master/slave with read routing

Register a master and one or more slaves per project. Reads are routed by a strategy you pick; writes go to the master.

import { ReplicationManager } from '@mostajs/replicator'
import { ProjectManager } from '@mostajs/mproject'

const rm = new ReplicationManager(new ProjectManager())

await rm.addReplica('secuaccess', {
  name: 'master', role: 'master',
  dialect: 'postgres', uri: 'postgresql://u:p@master:5432/secuaccess',
})
await rm.addReplica('secuaccess', {
  name: 'slave-1', role: 'slave',
  dialect: 'postgres', uri: 'postgresql://u:p@slave1:5432/secuaccess',
  lagTolerance: 5000,                    // ms of lag tolerated before it's "stale"
})

rm.setReadRouting('secuaccess', 'least-lag')   // or 'round-robin' | 'random'
const readService = rm.resolveReadService('secuaccess')   // EntityService for reads

least-lag is the interesting one: reads go to whichever slave is freshest, so you get scale-out without serving badly stale data. getReplicaStatus() returns each replica’s role, status, lag and schema count for a dashboard.

Feature 2 — Failover in one call

When the master dies, promote a slave. The old master, when it comes back, rejoins as a slave.

await rm.promoteToMaster('secuaccess', 'slave-1')   // slave-1 is now master

And here is the honest part, straight from the docs: promoteToMaster() can lose in-flight writes that weren’t replicated yet. For replicas that must never lose data, set lagTolerance: 0 so promotion is blocked while the replica is behind. No silent data-loss-by-default.

Feature 3 — Cross-dialect CDC (the differentiator)

This is what no other JS/TS ORM does. Define a replication rule that captures changes on a source project and replays them on a target — across dialects. PostgreSQL → MongoDB, verbatim, as a rule:

rm.addReplicationRule({
  name: 'pg-to-mongo',
  source: 'secuaccess',          // PostgreSQL
  target: 'analytics',           // MongoDB
  mode: 'cdc',                   // 'snapshot' | 'cdc' | 'bidirectional'
  collections: ['users', 'clients'],
  conflictResolution: 'source-wins',
})
await rm.sync('pg-to-mongo')

Three modes, each with a precise meaning:

Under the hood a SchemaMapper validates the cross-dialect move before you run it — validateCrossDialect(source, target, schemas) returns a CompatReport flagging issues like string-length limits or paradigm mismatches (relational ↔︎ document). You find out the Mongo target can’t represent something at design time, not at 2 a.m.

Feature 4 — A consistency model that doesn’t lie

Replication is where libraries oversell. This one labels exactly what you get — three levels:

Level Guarantee
Local transactions on one replica ACID
Master → slave Eventually consistent (read-your-writes only on the master)
CDC rules Eventually consistent + at-least-once → rules must be idempotent (upsert on the target)

There is no distributed cross-dialect transaction — and the docs say so, pointing you to saga/compensation instead. That candour is the point: you can architect correctly because the contract is stated, not implied.

Feature 5 — Scaffold a running service

You don’t wire this by hand. A scaffolder emits a ready-to-run service:

npx mostajs-replicator-scaffold --dir . --force      # writes services/replicator.mjs

The generated service reconnects the whole topology from a persisted replicator-tree.json (replicas + rules + routing), resolves '*' collection wildcards by introspecting the master catalogue, and is safe to run on a cron or as a long-lived process. enableAutoPersist('replicator-tree.json') keeps that tree in sync as you change the topology — which is exactly what the companion @mostajs/replica-monitor dashboard reads (no DB connection needed) to show live lag and status.

Where it fits

@mostajs/replicator is one module of the @mostajs/orm ecosystem. Because every replica is a plain @mostajs/orm connection, the schemas you replicate are the same EntitySchema your app already runs on — one mental model from CRUD to CDC. For live supervision pair it with @mostajs/replica-monitor; to move bulk data outside a live rule, see @mostajs/orm-copy-data.

Get started

npm install @mostajs/replicator @mostajs/orm @mostajs/mproject
npx mostajs-replicator-scaffold --dir .       # then edit services/replicator.mjs

If cross-dialect replication as a library (not an ops project) is useful to you, a ⭐ on GitHub helps — it’s the signal AI dev tools use to surface the package.

Tags: #nodejs #typescript #database #replication #cdc #postgresql #mongodb #cqrs #orm #failover #eventual-consistency #mostajs

Auteur : Dr Hamid MADANI drmdh@msn.com