Local Dev Loop (dual-profile, per-developer)¶
Run the Next.js editor on your laptop with hot-reload (~1s), pointed through an SSH tunnel at a backend on the staging VPS. The editor is backend-agnostic (it always talks to localhost:8000/7860/3002), so which backend you test against is purely which target the tunnel forwards to. Two profiles, one command to switch:
- isolated (default) — your own per-dev backend (API + MariaDB + Postgres + Asterisk + Workflow). Safe to edit anything; runs your local
api/branch. No live trunk / no registered phones, so telephony can't complete. - staging-backed — the shared staging-root backend (
:8000, live trunk + real registrations). Verify-only: writes hit the real staging DB.
A third toggle, ./dev backend livetrunk, keeps the isolated tunnel but points your dev-api's AMI at the shared live Asterisk, so your api/ branch can complete real calls before a PR.
The whole loop is managed by pm2 via the ./dev wrapper at the repo root. This replaces the old single-tunnel loop.
Staging VPS only — never tunnel to prod
Every tunnel target — both profiles — is on the staging VPS (94.136.188.221). Never point a forward at prod 147.93.168.216.
Staging outbound calls egress through PROD's trunk
The staging org's outbound trunk (org_mna9x47k_tata → 10.10.10.1) is reachable only via the prod WireGuard, so a livetrunk/staging-backed outbound call actually leaves through prod's cloud Asterisk → NUC → Tata. Fine for caller-ID / connectivity testing — just know real PSTN minutes and the prod trunk are involved.
Who gets what¶
Each developer has an isolated Linux user on 94.136.188.221. Nothing is shared between devs except the Pipecat gateway (:7860).
| Developer | SSH user | API | Workflow | Asterisk AMI / ARI | MariaDB | Postgres |
|---|---|---|---|---|---|---|
| Hari | burgundy | :8100 | :3102 | :5138 / :8188 | dev_burgundy_db | workflow_burgundy_db |
| Muthu | black | :8200 | :3202 | :5238 / :8288 | dev_black_db | workflow_black_db |
| Kailash | white | :8300 | :3302 | :5338 / :8388 | dev_white_db | workflow_white_db |
The staging root instance (:8000, pbx_api_db, gateway :7860) is shared and untouched. The gateway (/opt/pipecat-flow, :7860) is the one backend all devs share.
Test org seeded in every dev DB: AstraPrivate — 7f3d2fd5-347e-4cc2-a2d0-a9a5e0f78f79.
Telephony test number
Use 9944421125 as the destination for click-to-call / outbound call tests — it's a known test line, safe to ring repeatedly. (Live-call tests require the shared trunk; see the staging-backed / livetrunk profile.)
Architecture¶
Browser (localhost:3000)
│
▼
Next.js dev server (your laptop) burgundy (Hari) — isolated
│ /api/pbx/* ──► localhost:8000 ──┐ SSH tunnel API :8100 (dev_burgundy_db)
│ /api/gateway/* ──► localhost:7860 ──┼─► 94.136.188.221 gateway :7860 (SHARED)
│ /api/workflow/* ──► localhost:3002 ──┘ workflow :3102
│
└─ Firebase Auth ──► Google (direct, NEXT_PUBLIC_ASTRADIAL_ENV=staging)
next.config.ts rewrites /api/pbx/* → localhost:8000, /api/gateway/* → localhost:7860, /api/workflow/* → localhost:3002. The tunnel maps those to your dev user's ports (8000→8100 etc.), except 7860→7860 (shared gateway).
One-time setup¶
# 1. Clone + branch off staging (never off main / whatever was checked out)
cd ~/AstradialDevelopment
git clone https://github.com/astradial/astradial-platform.git
cd astradial-platform && git fetch origin && git checkout -B feat/my-thing origin/staging
# 2. Editor deps
cd editor && npm install
# 3. Key-auth to your dev user (so the tunnel + sync don't prompt for a password)
ssh-copy-id burgundy@94.136.188.221 # password: see the Dev Environment guide
# 4. Editor .env.local — copy from staging, then confirm the proxy targets
scp burgundy@94.136.188.221:/opt/pipecat-flow-editor/.env ./.env.local
# ensure these (and NEXT_PUBLIC_ASTRADIAL_ENV=staging) are present:
# NEXT_PUBLIC_PBX_URL=http://localhost:8000
# NEXT_PUBLIC_GATEWAY_URL=http://localhost:7860
# NEXT_PUBLIC_WORKFLOW_URL=http://localhost:3002
# GATEWAY_ADMIN_KEY=<48-char key from staging> (needed for the admin dashboard)
# ADMIN_USERNAME=admin ADMIN_PASSWORD=<from staging> (needed for org impersonation)
# 5. The sync-api.sh script (in the repo root) — for hot-reloading API code
./sync-api.sh # prints usage; needs your dev-user name
NEXT_PUBLIC_ASTRADIAL_ENV=staging is mandatory
Without it the editor treats the Firestore root as prod and any write corrupts production data. localhost must also be in the misssellerai Firebase project's Authorized domains (one-time, project-wide).
Run the loop (./dev — pm2-managed)¶
Prereqs (one-time): npm i -g pm2 and brew install autossh fswatch.
./dev up isolated # start tunnel + editor (+ api sync) under pm2 (default profile)
./dev status # active profile + backend target + chain health (tunnel→backend→editor)
./dev logs dev-editor # tail a service (dev-tunnel | dev-editor | dev-sync)
./dev stop # stop & remove all dev-* apps
Switch what the editor talks to — the editor process stays up (just hard-refresh the browser after a switch):
./dev switch staging # editor → shared staging-root backend (live trunk) — VERIFY ONLY
./dev switch isolated # editor → your isolated backend (default)
./dev backend livetrunk # (isolated tunnel) dev-api AMI → shared LIVE Asterisk → real calls
./dev backend isolated # (isolated tunnel) dev-api AMI → your own Asterisk (no trunk)
| Mode | editor → API | dev-api → Asterisk | Live trunk | Runs your api/ | Safe to edit data |
|---|---|---|---|---|---|
up isolated (default) | burgundy :8100 | burgundy :5138 | no | yes | yes (isolated DB) |
backend livetrunk | burgundy :8100 | shared :5038 | yes | yes | calls are REAL — verify only |
switch staging | staging-root :8000 | (its own) | yes | no (deployed api) | NO — real staging DB |
(DEV_USER=black|white ./dev up isolated for Muthu / Kailash. autossh is run under pm2 with autorestart:false so its own reconnection isn't double-driven; dev-sync runs only in the isolated profile.)
Editor is on :3000, not :3001
Turbopack ignores PORT from .env.local. ./dev status probes :3000.
Pre-PR test matrix — which mode validates what¶
| Feature area | Validate in | Why |
|---|---|---|
| Editor UI / dialogs / skeletons / caller-ID dropdown logic / config generation / user-DID-workflow CRUD | isolated | frontend + your api/ branch, isolated DB — safe |
api/ originate / AMI / ARI paths that need a call to complete | isolated + backend livetrunk | your api/ branch + the live trunk |
| Registration status / live-calls panel / trunk routing against the deployed api | staging-backed (switch staging) | needs the deployed api + live trunk; verify-only |
Looks like a bug, but it's a basement limitation
On plain isolated the Asterisk has no live trunk and no registered phones, so these are EXPECTED — not bugs: click-to-call never completes, endpoints show unregistered, the "Asterisk unreachable" banner. Run ./dev backend livetrunk (or switch staging) before debugging telephony. If it still fails there, then it's a real bug.
Dialplan-generation changes aren't fully live-testable locally
A change to the dialplan generator only shows up in your isolated Asterisk (which has no trunk); livetrunk and staging-backed use the shared deployed dialplan. Verify the regenerated ext_*.conf is correct in isolated, then confirm the live caller-ID/routing effect on staging after deploy.
Stopping the loop (./dev stop) does not touch your VPS backend — the remote pm2 dev-api / dev-workflow keep running on the VPS.
Disconnect / reconnect the VPS backend (session wrap-up)¶
./dev stop only tears down the local pm2 apps (tunnel / editor / sync); your remote dev-api keeps idling on the staging box. To fully disconnect it at the end of a session (free your dev API port), stop it on the VPS — then start it again next session. Replace burgundy with your own dev user (see Who gets what):
# wrap up — stop your VPS dev-api (frees :8100 for burgundy)
ssh root@94.136.188.221 'su - burgundy -c "pm2 stop dev-api"'
# next session — bring it back (resumes with the same nodemon --watch src setup)
ssh root@94.136.188.221 'su - burgundy -c "pm2 start dev-api"'
pm2 stop keeps the process definition (so pm2 start dev-api resumes it as-is) — it does not delete it. Leave dev-workflow running unless you also need to free its resources; it's a long-lived fixture, not per-session.
Logging in (admin → org impersonation)¶
- Open
http://localhost:3000, log in with the Firebase account (admin@astradial.com). You land on the platform-admin dashboard. - Click into an org (e.g. AstraPrivate). The org layout calls
/api/admin/impersonate/<orgId>(usesADMIN_USERNAME/ADMIN_PASSWORD→ the dev API's/admin/auth→ mints an org-owner token). Org pages then load live data.
Admin session 401s on org data — that's by design
Console shows [auth] 401 ignored — admin session protected until impersonation completes. If org pages stay empty, reload once — the first load races the impersonation token; the second uses the cached one.
Edit loop¶
- UI (
.tsx/.ts/.css) → save → Fast-Refresh ~1s. No tunnel/API restart. - API (
api/src/**) → save →sync-api.shrsyncs (~2s) → nodemon restartsdev-api. Test instantly. - DB migration → run it against your dev DB:
ssh burgundy@… 'cd ~/astrapbx && node scripts/run-migrations.js'.
Hot reload does not cover next.config.ts, middleware.ts, .env.local, or a new dependency — restart npm run dev for those.
⚠️ Gotchas (these burned us — read before debugging "my changes do nothing")¶
1. A stale orphan squatting your dev API port¶
The single biggest trap. A leftover process from a personal user dir (e.g. /home/hari/astrapbx, owned by user hari, on dev_hari_db) can bind your dev API port (:8100). Then your pm2 dev-api (user burgundy) can't bind it and crash-loops, while the orphan silently serves old code on the wrong DB. Symptoms: code/DB syncs "don't take effect"; /health uptime is days old; login behaves like an old build.
# Who actually owns :8100? If it's not your pm2 dev-api (right user/age/cwd) → orphan.
ssh root@94.136.188.221 'P=$(ss -ltnp "sport = :8100" | grep -oE "pid=[0-9]+" | head -1 | cut -d= -f2); ps -o user,etime,args -p "$P"; readlink /proc/$P/cwd'
# Fix: stop the orphan (via its owner pm2, or kill the PID), then start yours:
ssh root@94.136.188.221 'sudo -u <orphan-user> pm2 delete all; sudo -u burgundy pm2 restart dev-api'
pm2 dev-api runs via nodemon --watch src, so once it owns the port, sync-api.sh edits auto-restart it.
2. The :7860 gateway tunnel is required¶
Without -L 7860:localhost:7860, the admin dashboard (org list) 500s (api/gateway/admin/orgs). It's the shared Pipecat gateway, not part of your dev API. The old runbook's tunnel command omitted it.
3. Keep your dev API + workflow code in sync with staging¶
/home/<user>/astrapbx and /home/<user>/workflow-engine drift from staging. Resync (preserving your .env):
ssh root@94.136.188.221 '
rsync -a --delete --exclude=.env --exclude=node_modules --exclude=.git /opt/astrapbx/ /home/burgundy/astrapbx/
rsync -a --delete --exclude=.env --exclude=node_modules --exclude=.git /opt/workflow-engine/ /home/burgundy/workflow-engine/
chown -R burgundy:burgundy /home/burgundy/{astrapbx,workflow-engine}
sudo -u burgundy bash -lc "cd ~/astrapbx && npm install; cd ~/workflow-engine && npm install; pm2 restart dev-api dev-workflow"'
A stale workflow-engine shows up as Workflow Scheduler/Workflow DB down with note "engine /health has no scheduler/db field (old build?)".
4. Re-clone your dev DB from staging¶
ssh root@94.136.188.221 'mysqldump --single-transaction --routines pbx_api_db | mysql dev_burgundy_db && sudo -u burgundy pm2 restart dev-api'
5. Expected "down" in the dev Services panel¶
WireGuard Poller is down for dev users — it needs root to read the host's shared wg interface, and dev users are non-root by design. Harmless.
The PR flow¶
edit on localhost → verify in-browser on localhost:3000 → commit + PR to staging
│ │
(this loop) merge → CI deploys staging
│
QA on stageeditor.astradial.com
│
Promote staging → main → prod
git add editor/ <specific paths> # never `git add -A`
git commit -m "..."
git push -u origin feat/my-thing
gh pr create --base staging --title "..."
Never push to staging or main directly.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
Code/DB syncs "don't take effect"; /health uptime is days old | Orphan owns your dev port | Gotcha 1 |
Admin dashboard: api/gateway/admin/orgs 500 | :7860 not tunnelled / GATEWAY_ADMIN_KEY wrong | Gotcha 2 |
Org pages empty, [auth] 401 ignored — admin session protected | Impersonation racing / failed | Reload once; check ADMIN_PASSWORD |
Workflow Scheduler/DB down — "old build?" | Stale workflow-engine | Gotcha 3 |
ECONNREFUSED localhost:8000, or dashboard 500 on admin/orgs + 502 fetch failed on services-health | Tunnel dropped (plain ssh -N exits 255 on a link blip) | Restart Terminal 1 — and use autossh (see Run the loop) so it self-heals instead of dropping again |
Dashboard data all 0 / real prod orgs visible | NEXT_PUBLIC_ASTRADIAL_ENV missing/wrong | Stop; fix env; restart |
EADDRINUSE :::3000 | Port busy | lsof -ti:3000 \| xargs kill |
See also: Staging Environment, Staging Direct-Edit, CI/CD Pipeline.