Files
api/deploy/DEPLOY.md
T
mohmdsdgandCursor e98831f0f9 feat: add npm run update for safe API deploys
Ship schema to replicas without the data repair that disables subscriptions and stalls replication.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-07 17:15:12 +03:30

192 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```text
/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
```bash
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`
```bash
cd /var/www/superadmin/api
cp .env.example .env
nano .env
```
Required:
```env
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
```bash
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
```bash
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`).
```bash
cd /var/www/superadmin/web
npm ci
npm run build
# dist → /var/www/superadmin/web/dist
```
## 6) Nginx
```bash
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:
```bash
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:
```bash
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:
```bash
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:
```bash
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_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`).