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>
198 lines
6.4 KiB
Markdown
198 lines
6.4 KiB
Markdown
# 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 **publisher** (`/var/www/superadmin/api`):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
npm run replicas:check # schema + subscription + row counts (settings, tenants, …)
|
||
```
|
||
|
||
Force data realign from publisher (rare — e.g. after fixing subscriber permissions):
|
||
|
||
```bash
|
||
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`).
|