# New Production Environment — Major-Deploy Runbook

> **⚠️ SUPERSEDED for the actual asqvox cutover (2026-07-04).** The prod move is happening on the
> **SAME cPanel server** (asqvox reuses the `aiw-prod` slot), not a new dedicated box — see the
> **active** runbook **`deploy/ASQVOX-SAMEBOX-CUTOVER-RUNBOOK.md`** (which cross-references THIS doc
> for the deep shared detail: payload manifest, env boot-blockers, image-fingerprint checks, Retell
> per-widget wiring, two-pass token specifics). Keep THIS doc as that reference + for the "brand-new
> dedicated VPS" variant (its bootstrap/nginx/certbot sections apply if a fresh box is ever used).
> The concrete asqvox nginx includes the cutover runbook needs are ready at
> `deploy/cpanel-nginx/asqvox/*` + `deploy/cpanel-nginx/aiandhumn-301-to-asqvox.conf`.
>
> **Purpose.** Stand up production on a **different environment** (a new server, off the shared
> cPanel/WHM box at `65.254.80.37`) with the latest code from all forks. Written 2026-07-02;
> re-synced 2026-07-04 to `origin/main` HEAD **`d3c8ba2`**.
>
> **⚠️ DOMAIN PIVOT (new, `d3c8ba2` 2026-07-04):** `aiandhumn.com` is now being **decommissioned
> in favour of `asqvox.com`** — the latest commit noindexes the apex + app and drops its sitemap
> so search deindexes the dying domain, while stating *"the same codebase becomes asqvox.com and
> must stay indexable."* **This very likely makes the "new production environment" the
> `asqvox.com` stack, not an aiandhumn.com like-for-like move.** That flips a core assumption of
> the "invisible move" path below — see **§1** for the two variants and the one decision that
> selects between them. Everything about payload/build/data below is domain-agnostic and holds
> either way; only the DNS/OAuth/SMTP/token/webhook wiring differs.
>
> **Why move:** the shared host is oversubscribed (5.5 GB RAM / 80 GB disk that has hit 92%,
> ~25 client cPanel sites) and heavy `docker compose build` runs have corrupted containerd shims
> there twice (see `handoff.md` 2026-06-13 incident + memory `ops-vps-build-shim-crash`).
> Production should not share fate with client-site hosting or dev builds. The current shared
> box keeps running DEV (`aiworkfllow.com`) unchanged.

---

## 0. Payload manifest — what "latest code" means

| Item | Value |
|---|---|
| Code | `origin/main` HEAD (readiness verified at **`d3c8ba2`**, 2026-07-04) |
| Migrations | **`0000` → `0029`** — 30 journal entries, monotonic, verified (unchanged since 0029; no new migration in the Jul-04 commits) |
| Services | **7**: `postgres`, `migrate`, `api`, `app` (SPA), `widget`, `cafe`, `marketing` |
| Compose | `deploy/prod/docker-compose.yml` (project `aiw-prod`, net `aiw-prod-net`, volume `aiw-prod-pgdata`) |
| Loopback ports | api `3020` · app `5183` · widget `5184` · marketing `5185` · cafe `5186` (unchanged — zero compose edits needed) |
| nginx vhosts | 5 hosts: apex (=marketing, +www 301), `api.`, `app.`, `widget.`, `cafe.` — sources in `deploy/cpanel-nginx/aiandhumn/` (retarget hostnames if the domain is `asqvox.com`) |
| New env vars | **none** since the last sync — the updated `.env.example` templates (2026-07-02) are still complete |

**Delta vs the live prod (`371c2f6`, DB at `0028`, deployed 2026-06-29):** 16 commits. Two waves:
1. **Cafe vertical + marketing** (`058c182` and earlier): backend voice-ordering relay, standalone
   `aiw-cafe-widget`, `boojee-cafe-app` SPA, cafe deploy infra, menu-KB + ordering bugfixes,
   migration **`0029_widget_cafe_ordering`**, killswitch **`AIW_CAFE_DISABLED`**.
2. **Jul-04 hardening wave** (`661f563` → `d3c8ba2`, 8 commits): cafe-widget greeting-shield +
   hold-on-pause + live-state UI + SDK-listener-dedupe + audit fast-follows (#6/#8/#9); **backend
   `trustProxy` narrowed** from `true` to `['loopback','linklocal','uniquelocal']` (XFF-spoof fix
   `#7` — **deploy-relevant, see §2b**); build-time cafe/DIY widget isolation; and the
   **aiandhumn noindex** SEO change (apex + app noindex, robots stays crawlable). **No migration,
   no new env var in this wave.**

Everything else (Pricing F1–F11, co-browse, contact-capture sync, anti-abuse, spike alerts,
marketing site, rebrand, SEO) is already in the live prod image and rides along unchanged.

**Nothing is unmerged:** `feature/cobrowse-t0-t1` and `prod-cobrowse-release` are stale branches
whose content was absorbed into `main` — never deploy from them.

**Uncommitted fork WIP in the shared checkout (do NOT bundle into a deploy build):** at sync time
the working tree also carried un-pushed fork edits — `aiw-voice-marketing/app/about/about.content.ts`
(modified), and two untracked docs `deploy/MARKETING-DEPLOY-RUNBOOK.md` + `boojee-cafe-app/boojee-on-asqvox.md`.
These are not on `origin/main`; a VPS `git pull` won't see them. Confirm they're either pushed or
intentionally excluded before the prod build so the deployed image matches `origin/main`.

---

## 1. Decisions locked / assumptions

> **✅ DECISIONS LOCKED (user, 2026-07-04):** new domain **`asqvox.com`** (Variant A) + **fresh
> empty DB** (§4 fresh-start is now the PRIMARY path). Variant B (aiandhumn like-for-like) and the
> data-migration path are NOT chosen — kept below only as discarded reference. Consequence chain to
> internalise: fresh DB ⇒ every widget row is gone ⇒ the committed widget tokens are invalid ⇒ the
> marketing/app/cafe frontend builds must be re-baked with **new asqvox tokens created in the fresh
> dashboard AFTER first deploy** ⇒ two build passes (bootstrap with placeholders, then re-bake).

### Variant A — new domain `asqvox.com` ✅ CHOSEN
The move is **NOT** invisible. Everything domain-bound must be re-created / re-pointed:
- **6 DNS records** on `asqvox.com`: `@`, `www`, `api`, `app`, `widget`, `cafe` → new box IP.
- **New OAuth apps** (Google + LinkedIn) with `https://api.asqvox.com/api/auth/{google,linkedin}/callback`
  redirect URIs; LinkedIn "Sign In with OpenID Connect" product per app.
- **Backend env** retargeted: `APP_PUBLIC_BASE_URL=https://api.asqvox.com`,
  `APP_FRONTEND_URL=https://app.asqvox.com`, `WIDGET_ALLOWED_ORIGINS=https://app.asqvox.com,https://asqvox.com`,
  `APP_BASE_URL`, `SMTP_FROM` sender (verify the `asqvox.com` domain at the SMTP provider),
  `OPS_SPIKE_ALERT_EMAIL` / `ENTERPRISE_LEAD_EMAIL` if the alias domain changes.
- **Frontend env** retargeted + rebuilt: `aiw-voice-app/.env.production`,
  `aiw-voice-marketing/.env.production`, `boojee-cafe-app/.env.production` all carry `api./app./widget./`
  `aiandhumn.com` URLs today → change to `asqvox.com` and rebuild those images (the URLs are baked at build time).
- **nginx**: copy the `deploy/cpanel-nginx/aiandhumn/` fragments but **rename the hostnames to
  `asqvox.com`** and **DROP the `X-Robots-Tag: noindex` blocks** — asqvox.com must be *indexable*
  (the noindex was aiandhumn-only, to deindex the dying domain). Keep the www→apex 301, HTTPS
  forcing, SSE `proxy_buffering off`.
- **Retell webhook URLs** per agent → `https://api.asqvox.com/api/webhooks/retell` (re-sync via link-agent).
- **Widget tokens**: if the DB migrates (below), tokens survive — but the committed bundles still
  point `data-backend-url`/bundle-src at `aiandhumn.com`. The marketing/app/cafe builds above fix
  the marketing+cafe orbs; **customer embed snippets on external sites keep working only if
  `api.aiandhumn.com` stays alive** (keep the old box's `api.` answering, or have customers re-paste
  the `asqvox.com` snippet). Decide the cutover grace window.

### Variant B — same domain `aiandhumn.com`, just a new server ❌ NOT CHOSEN
(Discarded reference.) Would have been invisible to every external integration; only the 6
A-records move. Superseded by the asqvox switch.

**Locked regardless of variant:**
- **DEV stays on the old box** (`aiworkfllow.com` stack untouched).
- **Same compose files, same ports** — `/opt/aiw/{src,prod}` layout reproduced verbatim on the new box.
- Razorpay/Paddle keys **blank pending KYC** (gateway endpoints 503 fail-closed). `PADDLE_API_BASE`
  stays `https://api.paddle.com` (env.ts allowlist). `WIDGET_CHALLENGE_SECRET` + `COBROWSE_SUB_SECRET`
  must be real values or the API refuses to boot (`assertProdSecret`).

---

## 2. New-server sizing + bootstrap (one-time)

**Sizing guidance** (from observed usage on the shared box: both stacks ≈ 580 MB RAM at idle;
disk pain came from builds + logs, not data):

- ≥ 2 vCPU, **≥ 8 GB RAM**, **≥ 80 GB SSD** — build headroom is the point of the move; don't recreate the disk-pressure failure mode.
- Any mainstream region works; India/UAE-adjacent (e.g. Mumbai/Bahrain/Singapore) shaves latency for the primary customer base.

**Bootstrap:**

```bash
# 1. Docker Engine + compose plugin (Ubuntu 22.04/24.04 assumed)
curl -fsSL https://get.docker.com | sh

# 2. Log rotation FROM DAY ONE (the old box learned this the hard way —
#    a 168 MB crash log helped fill the disk)
cat | sudo tee /etc/docker/daemon.json << 'JSON'
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }
JSON
sudo systemctl restart docker

# 3. Repo + env layout (mirrors the old box exactly)
sudo mkdir -p /opt/aiw && sudo chown $USER /opt/aiw
git clone https://github.com/dhruvdholakia/AIWoiz.git /opt/aiw/src
mkdir -p /opt/aiw/prod/storage
ln -s /opt/aiw/src/deploy/prod/docker-compose.yml /opt/aiw/prod/docker-compose.yml

# 4. nginx (host-level, owns 80/443) + certbot
sudo apt-get install -y nginx certbot python3-certbot-nginx
```

### 2a. The `.env` — start from the template, reuse provider secrets, retarget the domain

On the **asqvox fresh-start** path, do NOT blind-copy the old `.env` (its URLs + OAuth are
aiandhumn's and a fresh DB has no sessions worth preserving). Instead: `cp deploy/prod/.env.example
/opt/aiw/prod/.env && chmod 600` and fill it:

- **Reuse from the old box** (provider-level, domain-agnostic): `RETELL_API_KEY`, `ANTHROPIC_API_KEY`,
  `SMTP_*` (re-verify the sender domain covers `asqvox.com`), Bolna/Vapi keys if any.
- **Generate fresh** (new DB — no reason to carry): `POSTGRES_PASSWORD`, `JWT_SECRET`, `ADMIN_API_KEY`,
  `WIDGET_CHALLENGE_SECRET`, `COBROWSE_SUB_SECRET` (all `openssl rand`).
- **Retarget to asqvox**: `APP_PUBLIC_BASE_URL=https://api.asqvox.com`,
  `APP_FRONTEND_URL=https://app.asqvox.com`, `APP_BASE_URL=https://api.asqvox.com`,
  `WIDGET_ALLOWED_ORIGINS=https://app.asqvox.com,https://asqvox.com`,
  `ADMIN_DASHBOARD_URL=https://api.asqvox.com/admin/`.
- **New OAuth apps** for asqvox → `GOOGLE_CLIENT_ID/SECRET`, `LINKEDIN_CLIENT_ID/SECRET` with
  `https://api.asqvox.com/api/auth/{google,linkedin}/callback` redirect URIs.

Boot-blockers if missing/wrong (`NODE_ENV=production` makes these fatal at startup, by design):

| Var | Requirement |
|---|---|
| `WIDGET_CHALLENGE_SECRET` | real value (not dev default) — `assertProdSecret` |
| `COBROWSE_SUB_SECRET` | real value (not dev default) — `assertProdSecret` |
| `PADDLE_API_BASE` | `https://api.paddle.com` (allowlist-validated) |
| `AIW_CRON_DISABLED` | `false` — replenishment/top-up-expiry/email/spike crons must run |
| `AIW_SRC` | `/opt/aiw/src` (compose build-context interpolation) |

Keep `JWT_SECRET` **identical** to the old box (same env, same domain — carrying it over keeps
existing logins, OAuth state HMACs, and challenge tokens valid across the cutover).

> **cPanel-target variant:** if the new box is another cPanel/WHM server, skip §2's nginx/certbot
> and instead follow `deploy/README.md` one-time setup (create account + subdomains, AutoSSL,
> user-includes) — the `deploy/cpanel-nginx/aiandhumn/` files install verbatim.

### 2b. nginx vhosts (plain-nginx adaptation)

The files in `deploy/cpanel-nginx/aiandhumn/*/proxy.conf` are **server-scope include
fragments** — on cPanel the outer `server{}` + TLS come from EA-nginx. On a plain box, wrap
each in a standard vhost. Per host `H` ∈ {apex+www, api, app, widget, cafe}:

```nginx
server {
    listen 80;
    listen 443 ssl http2;
    server_name <H>;                    # apex file also carries www + the 301
    ssl_certificate     /etc/letsencrypt/live/<H>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<H>/privkey.pem;

    # …then paste the body of deploy/cpanel-nginx/aiandhumn/<H>/proxy.conf here…
}
```

Adaptation notes (all minor):
- The fragments' `location ~ ^/(?!\.well-known/)` lookaheads and `$scheme` redirect guards
  were built to coexist with cPanel AutoSSL — they are harmless under certbot and keep
  HTTP-01 renewals working. Keep them.
- The apex fragment's comment about a cPanel-owned `location = /robots.txt` collision does
  **not** apply on a plain box — if you want, you may now add the exact-match robots.txt
  block that cPanel made impossible.
- `proxy_buffering off` is already present (required for the co-browse SSE stream); the 60 s
  read timeout comfortably covers the 15 s contact-collect hold.
- **On the `asqvox.com` variant, DROP the `X-Robots-Tag: noindex` blocks** that `d3c8ba2` added to
  the apex + `app.` fragments — those exist only to deindex the dying `aiandhumn.com`. asqvox.com
  must stay crawlable/indexable. (Keep them if you also front the old aiandhumn.com from this box.)

> **⚠️ `trustProxy` is now IP-range-scoped (`0f3f623`, deploy-critical).** The API changed from
> `trustProxy: true` to `trustProxy: ['loopback','linklocal','uniquelocal']`. It only believes
> `X-Forwarded-For` from a proxy whose peer IP is loopback (`127.0.0.1`) or a private range
> (Docker bridge `172.16/12`, etc.). **A plain nginx on the same host proxying to `127.0.0.1:302x`
> satisfies this — good.** But if you put **Cloudflare (orange-cloud proxy)** in front of the new
> domain, Cloudflare's edge IP is *public*, so proxy-addr stops there and **`req.ip` becomes the
> Cloudflare edge IP, not the visitor** → per-IP abuse caps, geo-IP, and audit logging all break.
> Keep the new domain's DNS **grey-cloud / DNS-only**, or add Cloudflare's ranges to the trust list
> (code change) before enabling the orange cloud. Verify post-deploy with a server-side log of
> `req.ip` on a real request showing the true client IP, not `172.x` or a CF edge.

**TLS chicken-and-egg at cutover:** certs can only be issued once DNS points at the new box
(HTTP-01), or beforehand via **DNS-01**. Recommended: issue via DNS-01 pre-cutover if the DNS
provider has API support; otherwise accept a ~1-minute cert gap right after the flip
(`certbot --nginx -d asqvox.com -d www.asqvox.com -d api.asqvox.com -d app.asqvox.com -d widget.asqvox.com -d cafe.asqvox.com`).

---

## 3. Build (pre-cutover — new box takes no traffic yet)

```bash
cd /opt/aiw/src && git pull origin main && git log -1 --oneline   # expect ≥ d3c8ba2
cd /opt/aiw/prod
docker compose config | grep -E '^\s+context:'   # all contexts under /opt/aiw/src — sanity

# On 8 GB+ you may build all at once; one-at-a-time remains the safe habit:
for s in migrate api app widget cafe marketing; do docker compose build $s; done
```

**Image fingerprint checks (the stale-image trap killer):**

```bash
# journal baked into the migrate/api image must have all 30 entries (0000→0029)
docker compose run --rm --no-deps migrate sh -c "grep -c '\"idx\"' drizzle/meta/_journal.json"   # → 30
# self-healing migrator present
docker compose run --rm --no-deps api sh -c "grep -c orphan-fix dist/lib/db/migrate.js"          # → 2
```

---

## 4. Fresh-start bring-up on `asqvox.com` (PRIMARY — locked path)

No dump/restore, no freeze window, no rollback-to-old-box needed — this is a **clean new product
instance** on a new domain. The trade is that widget tokens don't exist until after the first
deploy, so the frontends build **twice**: once with placeholders to stand the box up, once re-baked
with the real asqvox tokens. Because it's a brand-new domain, there's no live traffic to protect
during bring-up.

### Pass 1 — stand up the backend + get a working dashboard

```bash
cd /opt/aiw/prod
docker compose up -d postgres
docker compose up -d migrate          # applies ALL 30 migrations from zero
docker compose logs migrate           # expect: 30 pending → [apply] 0000…0029 → complete; ZERO [orphan-fix]
docker compose up -d api
docker compose exec api node dist/scripts/db-status.js   # → 30 entries / 30 applied / 0 drift / 0 pending
docker compose logs --tail 50 api     # "Server listening on :3000"; cron scheduler registered; no boot errors
curl -fsS http://127.0.0.1:3020/health
```

The `plans` seed rides in migrations `0004` + `0011`, so pricing works immediately. Build `app`
with the asqvox `.env.production` (see §4a) — its bundle needs no token to render login/signup —
then create the first tenant by signing up at `https://app.asqvox.com/signup` (or seed an ops
account with `aiw-voice-backend/src/scripts/create-staff` per commit `5ec9273`).

### Pass 1.5 — recreate widgets + Retell wiring in the fresh dashboard

Everything below is empty on a fresh DB and must be re-created (all one-time):
- **Widgets**: run the 7-step wizard for each needed widget → note each new **public token** (`wt_…`).
  Minimum three to restore the current sites: the **marketing orb**, the **SPA embed demo**, and the
  **Boojee cafe** widget.
- **Retell**: link each agent (`POST /api/admin/widgets/:id/link-agent`) so the webhook auto-syncs to
  `https://api.asqvox.com/api/webhooks/retell`; paste each per-widget **cobrowse command key** +
  **contact_sync_key** into the cloned Retell flow templates (docs `cobrowse-retell-setup.md`,
  contact-capture handoff, `boojee-retell-cafe-setup.md`). Publish each agent.

### Pass 2 — re-bake frontends with the real asqvox tokens, redeploy

Paste the new tokens into the asqvox frontend env files (§4a), then:

```bash
cd /opt/aiw/prod
docker compose build app marketing cafe    # tokens + URLs are baked at build time
docker compose up -d app marketing cafe widget
```

### DNS + TLS

- **6 records on `asqvox.com`** → new box IP: `@` (apex=marketing), `www`, `api`, `app`, `widget`, `cafe`.
- Issue certs for all six (DNS-01 pre-cutover, or `certbot --nginx -d asqvox.com -d www.asqvox.com -d api.asqvox.com -d app.asqvox.com -d widget.asqvox.com -d cafe.asqvox.com`).
- **Do NOT** copy the aiandhumn `noindex` nginx blocks — asqvox must be indexable (§2b).

### The aiandhumn embeds question (decide explicitly)

Fresh-start means the **old aiandhumn prod stays entirely separate and untouched** — its widgets,
tenants and `api.aiandhumn.com` keep working on the old box for as long as it runs. Any customer
site still embedding an `aiandhumn.com` snippet keeps talking to the OLD backend, not asqvox. Decide:
(a) leave aiandhumn running as-is for existing embeds, or (b) sunset it and ask those customers to
re-paste the asqvox snippet. The `d3c8ba2` noindex only affects *search visibility*, not the API.

### 4a. asqvox frontend env files (create before Pass 1 for URLs; re-edit tokens before Pass 2)

Clone each `.env.production` → asqvox URLs. Tokens are placeholders in Pass 1, real in Pass 2:

| File | URL keys → asqvox | Token key (Pass 2) |
|---|---|---|
| `aiw-voice-app/.env.production` | `VITE_API_BASE_URL=https://api.asqvox.com`, `VITE_MARKETING_BASE_URL=https://asqvox.com`, `VITE_*_WIDGET_BUNDLE_URL=https://widget.asqvox.com/aiw-voice-widget.js` | `VITE_MARKETING_WIDGET_TOKEN` |
| `aiw-voice-marketing/.env.production` | `NEXT_PUBLIC_API_BASE_URL=https://api.asqvox.com`, `NEXT_PUBLIC_APP_BASE_URL=https://app.asqvox.com`, `NEXT_PUBLIC_MARKETING_BASE_URL=https://asqvox.com`, `…_WIDGET_BUNDLE_URL=https://widget.asqvox.com/…` | `NEXT_PUBLIC_MARKETING_WIDGET_TOKEN` |
| `boojee-cafe-app/.env.production` | `VITE_CAFE_BACKEND_URL=https://api.asqvox.com` | `VITE_CAFE_WIDGET_TOKEN` |

> ### 4-alt. Data-migration path ❌ NOT CHOSEN
> (Discarded reference for a Variant-B move.) Would `pg_dump -Fc` the old prod DB + rsync
> `/opt/aiw/prod/storage`, restore on the new box, apply only `0029`, then flip DNS with the old
> box kept warm for rollback. Not used because we chose a fresh DB on a new domain.

---

## 5. Post-cutover verification (smoke matrix)

| # | Check | Expect |
|---|---|---|
| 1 | `curl -fsS https://api.asqvox.com/health` | 200 `{"ok":true,…}` |
| 2 | `https://asqvox.com/` | 200, marketing HTML (Next.js), orb present, **indexable** (no `X-Robots-Tag: noindex`) |
| 3 | `https://www.asqvox.com/` | 301 → apex |
| 4 | `https://app.asqvox.com/login` | 200 SPA; a freshly-signed-up account logs in |
| 5 | `https://api.asqvox.com/api/auth/google` | 302 to Google (new asqvox OAuth app) |
| 6 | `https://widget.asqvox.com/aiw-voice-widget.js` | 200, `application/javascript` |
| 7 | `https://cafe.asqvox.com/` | 200 Boojee app |
| 8 | Live voice call on the marketing orb (real asqvox token baked) | Retell session connects; transcript flows |
| 9 | `docker compose logs api` | `cron: scheduler registered`, replenishment/email/spike tick lines, no Zod/boot errors |
| 10 | `db-status` | 30/30/0 drift/0 pending |
| 11 | `POST /api/billing/webhooks/*` (no creds) | **503** (fail-closed — KYC pending, correct) |
| 12 | Authed SPA routes (`/api/account/*`) unauthenticated | 401 (mounted) |
| 13 | `req.ip` on a real request (server log) | true client IP, **not** `172.x`/CF-edge (trustProxy sanity, §2b) |
| 14 | Co-browse: live "take me to pricing" on a wired widget | navigates; SSE survives the proxy |
| 15 | Contact-capture pill on a wired widget | typed email lands in Leads |
| 16 | Cafe: voice "add butter chicken" on cafe.asqvox.com | cart fills (assistant never places the order) |

14–16 need the per-widget Retell wiring (§6). #13 catches the trustProxy/Cloudflare landmine early.

---

## 6. Retell-side wiring (per widget — full re-do on the fresh-start path)

Fresh DB ⇒ every widget + agent link is created new (§4 Pass 1.5). For each widget:

- Link the agent (`POST /api/admin/widgets/:id/link-agent`) → webhook auto-syncs to `https://api.asqvox.com/api/webhooks/retell`.
- Co-browse: flow template's `show_page` function URL (`https://api.asqvox.com/api/cobrowse/command`) + per-widget `X-AIW-Cobrowse-Key` header (`docs/cobrowse-retell-setup.md`).
- Contact-capture sync: `collect_contact` custom-function + per-widget `contact_sync_key`; agent config **en-IN**, Interruption Sensitivity **0.3**, Response Eagerness **low**, denoising **on**, backchannel **off**, digit-word boosted keywords → **Publish** (handoff 2026-06-29).
- Cafe: Boojee agent per `docs/boojee-retell-cafe-setup.md` (+ `docs/boojee-cafe-bugfix-runbook.md`).

---

## 7. Day-2 setup on the new box (do these this time)

- **Postgres backups:** daily `pg_dump -Fc` to off-box storage (cron + rclone/S3). The old box never got this.
- **Uptime monitoring:** Uptime Kuma (or similar) pinging `https://api.asqvox.com/health` + apex + app every minute.
- **Disk watch:** alert at 80% — the failure mode that motivated this move.
- **Old aiandhumn box:** untouched by this deploy (fresh-start = separate instance). Sunset it on its own timeline per the §4 embeds decision; its `noindex` is already deindexing it from search.

---

## 8. Known gaps riding along (unchanged by this deploy)

- Razorpay + Paddle keys pending KYC → F9 endpoints 503 fail-closed; activate later with `docker compose up -d --force-recreate api` after filling keys.
- `deploy/README.md` header table still describes the pre-cutover topology (apex = SPA) — the `cpanel-nginx/` artifacts and this runbook are current; README refresh is a follow-up.
- No automated tests / CI deploy — manual SSH per this runbook.
