Files
api/deploy/DEPLOY.md
T
mohmdsdgandCursor 810d1672a5 fix(replicas): never REFRESH with copy_data=true after a data copy
A second logical copy unique-conflicts on existing PKs and leaves
tablesync workers stuck, so live UPDATEs never apply on slaves.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 15:45:25 +03:30

6.4 KiB
Raw Blame History

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

  1. Open https://adm.montzar.ir → login
  2. https://adm.montzar.ir/get_user?domain=montpar.ir (or any known domain)
  3. https://adm.montzar.ir/get_settings?domain=k.montzar.ir (nginx must proxy /get_settings like /get_user)
  4. 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 publisher (/var/www/superadmin/api):

git pull
npm run replicas:sync      # 1) DDL on replicas  2) migrate deploy  3) repair data drift if any
npx prisma generate
npm run build
pm2 restart superadmin-api

Check only:

npm run replicas:check     # schema + subscription + row counts (settings, tenants, …)

Force data realign from publisher (rare — e.g. after fixing subscriber permissions):

npm run replicas:repair-data

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_URL at 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).