Codex Deploy Brief — Chimti Web Cutover to app.chimti.ai + admin.chimti.ai
You are deploying Chimti's new merged web app and its API updates on the Otlu
server (Coolify). Everything below is complete and self-contained — follow it
top to bottom. Do not improvise product changes; this is an infra task.
Context (30 seconds)
- `chimti-web-app` is ONE new repo that replaces BOTH old frontends
(`chimti-user-app`, `chimti-admin-app`). Same repo builds two variants via a
Docker build arg: `VITE_APP_VARIANT=user` or `admin`.
- Target domains (NEW): **https://app.chimti.ai** (user app) and
**https://admin.chimti.ai** (admin app).
- The API stays where it is: **https://chimti-api.otlu.io** — the web builds
default to this base URL (baked at build time). Do NOT move the API in this
task.
- `chimti-api` got two additive changes that must go live FIRST:
1) read-only billing routes (`/v1/invoices`, `/v1/plans`, `/v1/subscriptions`)
2) a background worker layer (`src/worker.js` + `src/workers/`, 21 jobs,
runs as a separate process off the same image; `npm run test:workers` = 25
tests green).
- Old apps at `chimti-user-app.otlu.io` / `chimti-admin-app.otlu.io` stay
deployed untouched as rollback. Do not delete them.
Step 0 — Preconditions (verify before touching Coolify)
Confirm these commits exist on the deploy branch of each repo (Jagjeet pushes
them; if missing, stop and tell him):
- `chimti-api`: `src/routes/billing.js` exists; `src/app.js` registers it;
`src/worker.js` + `src/workers/` (7 files) + `test/workers.test.js` exist;
`package.json` has `"worker": "node src/worker.js"` and `"test:workers"`.
- `chimti-web-app`: `Dockerfile`, `nginx.conf`, `.dockerignore` at repo root;
`src/` app code; `npm test` passes locally (token lint + module contract +
render smoke, both variants).
Sanity on a checkout: `cd chimti-api && node test/workers.test.js` → expect
"all worker tests green — 25 passed".
Step 1 — Deploy chimti-api update
1. Coolify → existing `chimti-api` app → Redeploy (same Dockerfile, no config
change needed for this step).
2. Verify:
curl -s https://chimti-api.otlu.io/v1/health
# expect JSON containing "service":"chimti-api"
# login with a real internal account, then:
TOKEN="<accessToken from POST /v1/auth/login>"
curl -s -H "Authorization: Bearer $TOKEN" https://chimti-api.otlu.io/v1/plans | head -c 300
curl -s -H "Authorization: Bearer $TOKEN" https://chimti-api.otlu.io/v1/invoices | head -c 300
# expect {"items":[...]} from both (empty items array is fine)
Step 2 — CORS for the new domains
In the chimti-api app's env (Coolify env editor), APPEND to `CORS_ORIGIN`
(comma-separated, keep every existing origin):
https://app.chimti.ai,https://admin.chimti.ai
Redeploy/restart chimti-api after saving. Verify preflight:
curl -s -o /dev/null -w '%{http_code}\n' -X OPTIONS https://chimti-api.otlu.io/v1/health \
-H 'Origin: https://app.chimti.ai' -H 'Access-Control-Request-Method: GET'
# expect 204/200, not 4xx
Step 3 — Worker process (new Coolify service)
1. Coolify → create a new app `chimti-worker` from the SAME `chimti-api` repo
(same Dockerfile). No domain, no public port.
2. Override the start command to: `npm run worker`
(If using the Dockerfile CMD instead, set custom command — the image's
default CMD starts the API; the worker MUST run `node src/worker.js`.)
3. Env = copy ALL of chimti-api's env (it needs `DATABASE_URL` etc.), PLUS:
WORKERS_ENABLED=1
WORKER_ALERT_EMAIL=me@jagjeetsinghsethi.com
WORKER_DIGEST_HOUR=8
WORKER_SNAPSHOT_HOUR=2
WORKER_DEMO_RESET_HOUR=4
(Optional per-job kill switches: `WORKER_<KEY>=0`; cadence overrides:
`WORKER_<KEY>_INTERVAL_SECONDS=...` — keys are in
`chimti-docs/architecture/workers-automation-plan.md`.)
4. Deploy, then verify from logs: lines `"chimti worker starting"` and
`"job scheduled"` (21 jobs). On first boot it creates `worker_heartbeats`
and `worker_snapshots` tables itself — no migration step.
5. DB check (psql on the server):
SELECT name, last_run_at, last_ok_at, last_error FROM worker_heartbeats ORDER BY name;
-- within ~2 minutes most short-interval jobs should have last_ok_at set
Step 4 — DNS for chimti.ai
At the chimti.ai DNS provider add two records pointing at the Otlu server
(same IP the *.otlu.io apps use):
app.chimti.ai A <server-ip> (or CNAME to the Coolify proxy hostname)
admin.chimti.ai A <server-ip>
TTL 300 while cutting over. Coolify/Traefik will issue Let's Encrypt certs
automatically once the apps below exist — no manual cert work.
Step 5 — Two web apps from chimti-web-app
Create TWO Coolify apps from the `chimti-web-app` repo, Build Pack =
Dockerfile (repo root). The Dockerfile is nginx-static; no runtime env needed.
**App 1 — `chimti-web-user`**
- Domain: `app.chimti.ai`
- Build args: `VITE_APP_VARIANT=user`
(`VITE_BACKEND_API_BASE_URL` defaults to `https://chimti-api.otlu.io` inside
the Dockerfile — do not set it unless pointing at staging.)
**App 2 — `chimti-web-admin`**
- Domain: `admin.chimti.ai`
- Build args: `VITE_APP_VARIANT=admin`
Deploy both. Build should end with vite `✓ built` and an nginx image.
Step 6 — Smoke checklist
User app — https://app.chimti.ai:
- [ ] Login page loads over HTTPS (cert valid), Chimti wordmark + blue theme
- [ ] Login with a brand login works; an INTERNAL login here is rejected with
the variant-guard message (expected behavior)
- [ ] Dashboard shows live numbers; dark-mode toggle persists on reload
- [ ] Orders register loads; open an order → 7 tabs; move status works
- [ ] Record a test payment; Payments register reflects it
- [ ] Customers → open a customer (5 tabs); add an internal note
- [ ] Create one throwaway order end-to-end; on a machine running the print
agent, Print tags reaches localhost:47844
- [ ] Order → Share summary → open the public link (it must use the
app.chimti.ai origin)
- [ ] Open on a phone: hamburger drawer, card-list registers (no sideways scroll)
Admin app — https://admin.chimti.ai:
- [ ] Internal login (SUPER_ADMIN/FOUNDER) works; brand login rejected
- [ ] Founder deck dashboard renders (MRR/ARR from /v1/subscriptions)
- [ ] Clients + Brands registers; brand details 10 tabs
- [ ] Plans / Subscriptions / Invoices / Transactions load
- [ ] Permissions page lists roles from DB
- [ ] Modules page loads and Save works (PATCH /v1/platform-module-settings)
API/worker after 24h (bonus check):
- [ ] `SELECT * FROM worker_snapshots ORDER BY day DESC LIMIT 5;` has yesterday's rows
- [ ] Founder digest email arrived at WORKER_ALERT_EMAIL around 08:00 IST server time
Step 7 — Old domains policy
- Leave `chimti-user-app.otlu.io` and `chimti-admin-app.otlu.io` running
untouched for now (instant rollback).
- After Jagjeet confirms the new domains are good (give it a few days), add
301 redirects from the old domains to app/admin.chimti.ai (Coolify redirect
rule or a tiny nginx app), and archive the old app repos.
Rollback
- Web: point users back to the old otlu.io apps (they were never touched);
or delete the DNS records for app/admin.chimti.ai.
- API: redeploy the previous chimti-api image from Coolify's deployment
history. Billing routes and workers are additive — no schema rollback needed
(worker tables are standalone and harmless).
- Worker: stop/disable the `chimti-worker` app; nothing else depends on it.
Explicitly OUT of scope for this task
- Moving the API to api.chimti.ai (later, separate change: new domain + CORS +
new web builds with the new base URL).
- chimti-website / pre-launch page / customer app / field mobile app.
- Android/iOS store builds (Capacitor; runs from Jagjeet's Mac).
Reference docs in the repos
- `chimti-web-app/docs/deploy.md` — build/variant summary
- `chimti-docs/deployment/chimti-web-cutover.md` — original runbook (otlu.io
domains; this brief supersedes the domain choice)
- `chimti-docs/architecture/workers-automation-plan.md` — all 21 jobs + env flags
- `chimti-docs/decisions/2026-08-01-chimti-web-merge.md` — decision record
Report back with: each step's status, the smoke checklist ticked, and the
worker_heartbeats output.