From a single-database Prisma app to a replicated, monitored multi-database system — the @mostajs stack, end to end (a real migration)

From a single-database Prisma app to a replicated, monitored multi-database system — the @mostajs stack, end to end (a real migration)

The capstone of the @mostajs/orm ecosystem. Not a toy: this walks FitZoneGym, a real 40-model Prisma app, through the whole stack — one thread, one EntitySchema.

TL;DR

We take a real Prisma app — FitZoneGym, 40 models — and, without rewriting application code, turn it into a system that:

  1. runs on PostgreSQL (or any of 13 databases) — via @mostajs/orm-cli + @mostajs/orm,
  2. scales reads across a cross-dialect replica with least-lag routing and failover — via @mostajs/replicator,
  3. captures changes on selected collections with CDC rules — same package,
  4. is observable on a live dashboard — via @mostajs/replica-monitor,
  5. is ready for fan-out backup — via @mostajs/orm-copy-data.

Steps 1–4 are wired in FitZoneGym today; step 5 is the natural next addition. Five packages, one schema model, no second mental model anywhere.

Day 0 — the app we start with

FitZoneGym is a production-shaped Next.js + Prisma app — 40 models, dozens of new PrismaClient(...) call sites, originally on MongoDB. It works, until the requirements arrive: the deployment target is Postgres, the team wants a read replica, and ops wants eyes on it. With stock Prisma each of those is a project. With @mostajs, each is a step — and they all share one fact: everything is the same @mostajs/orm EntitySchema.

Step 1 — escape the single database (orm-cli → orm)

One command migrated FitZoneGym onto @mostajs/orm:

cd FitZoneGym
npx @mostajs/orm-cli bootstrap     # codemod + install + convert + DDL

The codemod rewrote every new PrismaClient(...) site to a Prisma-compatible client from @mostajs/orm-bridge (originals saved as *.prisma.bak, fully reversible), converted prisma/schema.prisma to .mostajs/generated/entities.json (40 entities), and applied DDL. Then we pointed it at Postgres:

# .mostajs/config.env
DB_DIALECT=postgres
SGBD_URI=postgresql://devuser:***@localhost:5432/fitzonedb

Application code edited by hand: 0 — the same db.user.findMany(...) calls now run on Postgres. (Full story: the orm-cli deep-dive.) We now own a 13-database-ready schema.

Step 2 — a cross-dialect read replica + failover (replicator)

Here’s where it gets interesting. FitZoneGym’s real topology is a Postgres master with a SQLite slave — replication across dialects, set up with @mostajs/replicator:

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

const rm = new ReplicationManager(new ProjectManager())

await rm.addReplica('fitbymongo', { name: 'master-pg', role: 'master',
  dialect: 'postgres', uri: 'postgresql://devuser:***@localhost:5432/fitzonedb',
  pool: { min: 2, max: 20 } })
await rm.addReplica('fitbymongo', { name: 'slave', role: 'slave',
  dialect: 'sqlite', uri: './fitzonedb.db', lagTolerance: 5000 })

rm.setReadRouting('fitbymongo', 'least-lag')        // reads go to the freshest node

That’s the differentiator no other JS/TS ORM ships: the master is Postgres, the replica is SQLite — a durable embedded mirror you can ship, query offline, or fail over to. await rm.promoteToMaster('fitbymongo', 'slave') promotes it; lagTolerance: 0 on a critical replica blocks promotion while it’s behind, so failover never silently loses data.

Step 3 — capture changes with CDC rules (replicator)

FitZoneGym also defines CDC rules — incremental change capture on selected collections, idempotent (source-wins):

rm.addReplicationRule({
  name: 'cdc-fitbymongo', source: 'fitbymongo', target: 'fitbymongo',
  mode: 'cdc', collections: ['users', 'clients'], conflictResolution: 'source-wins',
})
rm.enableAutoPersist('.mostajs/replicator-tree.json')   // persist topology for the monitor

The honest contract (spelled out in the replicator deep-dive): CDC is eventually consistent and at-least-once, so rules are idempotent — and @mostajs/replicator can replay captured changes onto a different dialect when the target is another store, which is how you’d feed an analytics database from the Postgres primary.

Step 4 — make it observable (replica-monitor)

Step 3 persisted the topology to replicator-tree.json, so the dashboard needs no database connection — it reads that file (credentials are already masked, see the *** above) and streams live status. FitZoneGym ships a services/monitor.mjs; under the hood:

npx mostajs-monitor --tree .mostajs/replicator-tree.json --port 14499 --token SECRET
# http://127.0.0.1:14499 — master/slave roles, live lag + sparkline, CDC rule stats, activity feed

“Is the SQLite slave keeping up? did cdc-fitbymongo run?” becomes a glance. Zero external dependencies — pure node:http + vanilla JS. (the replica-monitor deep-dive.)

Step 5 — the next addition: fan-out backup (copy-data)

Replication is not backup. FitZoneGym doesn’t wire this yet — and that’s the honest state — but it’s one config away. A nightly job would read the primary once and write a SQLite copy + a SQL dump + a JSON archive, with @mostajs/orm-copy-data:

# backup-daily.conf
--source db --source-dialect postgres --source-uri "postgresql://devuser:pass@localhost:5432/fitzonedb"
--dest db        --dest-dialect sqlite --dest-uri "./backups/daily.sqlite"
--dest sql-dump  --dest-file "./backups/daily.sql"
--dest json      --dest-file "./backups/daily.json"
--schemas .mostajs/generated/entities.json --create-tables --batch-size 1000
# /etc/cron.d/fitzone-backup
0 2 * * * cd /app && npx mostajs-copy --commandFile backup-daily.conf >> /var/log/backup.log 2>&1

The --schemas .mostajs/generated/entities.json is the same file the bootstrap produced in Step 1 — one source of truth, from migration to backup.

The whole picture

Prisma app (FitZoneGym, 40 models)
        │  orm-cli bootstrap  (every PrismaClient site rewritten, 0 by hand)
        ▼
@mostajs/orm  ──  Postgres primary (master-pg)
        │
   replicator ── SQLite replica (slave, least-lag) + CDC rules (users/clients)
        │
 replica-monitor ── reads replicator-tree.json (no DB) ── live lag / rules / activity
        ┊
 orm-copy-data ── nightly fan-out backup  (the next step)

What ties it together isn’t glue code — it’s that every box is the same @mostajs/orm EntitySchema. The 40 entities you migrated are the entities you replicate, monitor and (next) back up. One model, learned once, all the way down.

Start anywhere

You don’t adopt all five at once — each stands alone, they compose when needed:

Get started

npx @mostajs/orm-cli bootstrap        # in your Prisma project — the on-ramp

If a database stack that grows with you — without a second mental model — is useful, a ⭐ on the repos helps. It’s the signal AI dev tools use to surface the packages.

Tags: #nodejs #typescript #database #prisma #migration #replication #cdc #cqrs #backup #postgresql #sqlite #orm #mostajs

Auteur : Dr Hamid MADANI drmdh@msn.com