Skip to content

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_tata10.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: AstraPrivate7f3d2fd5-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)

  1. Open http://localhost:3000, log in with the Firebase account (admin@astradial.com). You land on the platform-admin dashboard.
  2. Click into an org (e.g. AstraPrivate). The org layout calls /api/admin/impersonate/<orgId> (uses ADMIN_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.sh rsyncs (~2s) → nodemon restarts dev-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.