Ship schema to replicas without the data repair that disables subscriptions and stalls replication. Co-authored-by: Cursor <cursoragent@cursor.com>
6.8 KiB
Deploy — SuperAdmin on shared montime host
Host: 185.50.38.53 (SSH port 3031)
Domain: https://adm.montzar.ir
DB: Postgres superadmin (owner montime) — already created
API port: 3010 (3000=montime, 3001=forisanad)
Layout on server
/var/www/superadmin/
api/ # clone of controlPanel/api.git
deploy/ # nginx + pm2 + this guide (in api repo)
web/ # clone of controlPanel/web.git (Vue dist)
1) Clone / pull
sudo mkdir -p /var/www/superadmin
sudo chown montime:montime /var/www/superadmin
cd /var/www/superadmin
git clone https://get.feep.ir/controlPanel/api.git api
git clone https://get.feep.ir/controlPanel/web.git web
Later updates: git pull in each folder.
2) API .env
cd /var/www/superadmin/api
cp .env.example .env
nano .env
Required:
NODE_ENV=production
PORT=3010
DATABASE_URL="postgresql://montime:YOUR_PASSWORD@127.0.0.1:5432/superadmin?schema=public"
JWT_SECRET="long-random-production-secret"
JWT_EXPIRES_IN="7d"
CORS_ORIGIN="https://adm.montzar.ir"
PUBLIC_API_URL="https://adm.montzar.ir"
SEED_ADMIN_MOBILE="09128081392"
SEED_ADMIN_PASSWORD="change-me"
# CENTRAL_SMS_* as needed
Password = same Postgres password that works for psql -h 127.0.0.1 -U montime -d superadmin.
3) Install, migrate, build, seed
cd /var/www/superadmin/api
npm ci
npx prisma generate
npx prisma migrate deploy
npm run build
npm run prisma:seed
# optional first import:
# npm run prisma:import-users
4) PM2
pm2 start /var/www/superadmin/api/deploy/ecosystem.config.cjs
pm2 save
pm2 status
curl -s http://127.0.0.1:3010/api/health || true
5) Web build
Same-origin: leave VITE_API_BASE unset (defaults to /api).
cd /var/www/superadmin/web
npm ci
npm run build
# dist → /var/www/superadmin/web/dist
6) Nginx
sudo cp /var/www/superadmin/api/deploy/nginx-adm.montzar.ir.conf /etc/nginx/sites-available/adm.montzar.ir
sudo ln -sf /etc/nginx/sites-available/adm.montzar.ir /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
SSL: if Certbot already issued for adm.montzar.ir, paths in the conf should match. Otherwise:
sudo certbot --nginx -d adm.montzar.ir
7) Smoke
- Open
https://adm.montzar.ir→ login https://adm.montzar.ir/get_user?domain=montpar.ir(or any known domain)https://adm.montzar.ir/get_settings?domain=k.montzar.ir(nginx must proxy/get_settingslike/get_user)- Confirm uploads/logo URLs use
PUBLIC_API_URL
8) Migrations with logical replication (failover replicas)
superadmin_pub streams rows to the failover hosts (adm.montech.ir, adm.montest.ir).
Logical replication does not carry DDL: a migration that adds a column on the
publisher makes every subscriber fail with missing replicated columns and the
subscription stalls until that column exists there too. Because _prisma_migrations
is replicated, prisma migrate deploy on a subscriber wrongly reports
"No pending migrations".
So never run migrate deploy alone. On the publisher host:
cp deploy/replicas.env.example deploy/replicas.env # once; add real DSNs
bash deploy/sync-replica-schema.sh --init # once, when schemas already match
For every release, on the publisher (/var/www/superadmin/api) and on each replica API directory:
npm run update
On the publisher this pulls, installs, applies DDL on the replicas before prisma migrate deploy, refreshes subscriptions with copy_data=false, builds, and restarts superadmin-api. On a replica (no deploy/replicas.env) the same command only pulls, installs, generates, builds, and restarts. It does not run sync-replica-data.sh.
Check only:
npm run replicas:check # schema + subscription + row counts (settings, tenants, …)
Do not use npm run replicas:sync or npm run replicas:repair-data for a release. Both run the data repair, which disables a subscription and recopies rows without advancing the replication origin. The apply worker then dies replaying old WAL. npm run update is the release command.
replicas:sync runs sync-replica-data.sh --repair-if-needed automatically when
settings / setting_templates / tenants counts differ from publisher.
On the publisher API (superadmin-api), a replica watch also runs: ~8s after
boot, then every 60s, and immediately after tenant/settings writes. Drift is
healed without an operator command. Logical replication still streams live
INSERT/UPDATE/DELETE; the watch only fills missed history (WAL is not
rewound).
Each replica API user (montime by default) needs SELECT on all tables — the
repair script re-applies grants. REPLICA_DSNS must connect as a role that can
TRUNCATE + ALTER SUBSCRIPTION on the subscriber. The watch is idle unless
deploy/replicas.env exists on that host.
Keep migrations schema-only. After each migrate, subscribers get
ALTER SUBSCRIPTION … REFRESH PUBLICATION WITH (copy_data = false) so new
tables join the subscription without a second full copy (repair already
copied rows; a second copy unique-conflicts and stalls live UPDATEs).
9) Data changes without deploy (import settings, edit tenant, …)
Normal path: publisher writes → superadmin_pub WAL → slaves apply in
seconds. No replicas:sync for that.
Why LSN-healthy slaves can still have empty settings: logical replication
is a stream, not a mirror. received_lsn = latest_end_lsn means “caught up
with current WAL”, not “every historical row exists”. Rows inserted while apply
was broken, or while the table was not yet in the subscription, are never
replayed. Postgres will not go back.
That gap is healed automatically on the publisher API (watch every 60s +
right after import/edit). You do not run replicas:repair-data by hand
once this build is running on /var/www/superadmin/api.
| Action | What actually happens |
|---|---|
| INSERT/UPDATE/DELETE while subscription is ready | Live WAL — slaves update immediately |
New table after FOR ALL TABLES publication |
Not replicated until REFRESH PUBLICATION (deploy replicas:sync does this) |
| Missed history (this tenant’s empty settings) | Watch compares counts and recopies from publisher, then live WAL resumes |
npm run replicas:check / replicas:repair-data remain for operators; they are
not required for day-to-day imports after the watch is up.
Local development note
Prisma provider is PostgreSQL only. Local options:
- Docker:
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=superadmin postgres:16 - Or point
DATABASE_URLat the server DB only for short tests (prefer local DB)
SQLite history is kept under prisma/migrations_sqlite_archive/ (reference only; not applied by migrate deploy).