Appearance
Deployment
Deploy @aw-chat/server (and optionally @aw-chat/admin + this docs site) on your infrastructure. The API image runs TypeScript via tsx under tini.
This page is the devops handoff checklist. Fill every production item before pointing host apps at the new origin.
Handoff order
- PostgreSQL — provision DB; set
DATABASE_URL. Migrations apply on container start (db:migratestill available for one-offs). - Secrets / env — Intercom tokens,
JWT_SECRET(same as your app’s token issuer),ADMIN_SESSION_SECRET,FILE_URL_SECRET, S3, optional FCM/Telegram. See Configuration. - API container — build/run server Docker; health:
/health, ready:/ready(db+ Intercom breaker). - Intercom webhook — HTTPS
POST https://<api>/webhooks/intercomwith Client Secret (Webhooks). - Admin —
admin:createbootstrap user; deploy admin SPA; add its origin toCORS_ORIGINS. - Host apps — point SDK
serverUrlat the API; add app origins toCORS_ORIGINS; mint JWTs with the shared secret and matchingaud/iss.
Production checklist
Copy into the ticket / runbook:
- [ ]
NODE_ENV=production - [ ]
DATABASE_URL(prefer TLS in the connection string) - [ ] Intercom:
INTERCOM_ACCESS_TOKEN,INTERCOM_CLIENT_SECRET(INTERCOM_TICKET_TYPE_IDonly if user ticket creation is enabled) - [ ]
JWT_SECRET≥32 chars — same value as the host mint service - [ ]
REQUIRE_JWT_EXP=1 - [ ]
JWT_AUD+JWT_ISS— same strings the mint service puts in tokens - [ ] Distinct ≥32-char
ADMIN_SESSION_SECRETandFILE_URL_SECRET(≠JWT_SECRET) - [ ]
CORS_ORIGINS— comma-separated SDK + admin browser origins (no wildcards) - [ ]
TRUST_PROXY_HOPS=1(or exact hop count) behind nginx/Caddy/Coolify - [ ]
REQUIRE_S3=1+S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY - [ ]
ENABLE_DEVTOOLS/ENABLE_DEVTOOLS_RESET_ALLempty - [ ] TLS at reverse proxy; forward
X-Forwarded-For/CF-IPCountry - [ ] Origin locked: API not reachable by bypassing the edge (firewall / private network)
- [ ] Intercom webhook HTTPS registered to
https://<api-public>/webhooks/intercom - [ ] First admin:
pnpm --filter @aw-chat/server admin:create <user> <pass> [email] - [ ] Host apps mint short-lived JWTs (
exp,aud,iss,userId/sub) - [ ] Optional: FCM / Telegram / auto-reply /
DATA_RETENTION_DAYS
Production refuses to boot if the security asserts fail — Security. Mint contract and AW-side work: SEC-48 handoff.
Minimal production .env skeleton
bash
NODE_ENV=production
PORT=3100
DATABASE_URL=postgresql://awchat:...@db:5432/awchat
JWT_SECRET=<shared-with-mint-≥32>
REQUIRE_JWT_EXP=1
JWT_AUD=aw-chat
JWT_ISS=awp-api
ADMIN_SESSION_SECRET=<distinct-≥32>
FILE_URL_SECRET=<distinct-≥32>
CORS_ORIGINS=https://app.example.com,https://admin-chat.example.com
TRUST_PROXY_HOPS=1
INTERCOM_ACCESS_TOKEN=...
INTERCOM_CLIENT_SECRET=...
REQUIRE_S3=1
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET=aw-chat-files
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_REGION=us-east-1Docker (API)
Build from the monorepo root (workspace packages are required):
bash
docker build -f apps/server/Dockerfile -t aw-chat-server .
docker run --env-file apps/server/.env -p 3100:3100 aw-chat-serverHealth probes:
| Path | Use |
|---|---|
GET /health | Liveness (process up) |
GET /ready | Readiness (DB + Intercom breaker) — prefer private/ingress-only |
Admin panel
Bootstrap:
bash
pnpm --filter @aw-chat/server admin:create <username> <password> [email]Auth is HttpOnly cookie + CSRF header (not a Bearer token in sessionStorage).
Recommended: Docker image with same-origin nginx proxy
The admin image proxies /admin/* and /files/* to the API so CSP connect-src 'self' works.
bash
docker build -f apps/admin/Dockerfile \
--build-arg API_ORIGIN=chat-api.example.com \
--build-arg VITE_API_BASE= \
-t aw-chat-admin .| Build arg | Required | Meaning |
|---|---|---|
API_ORIGIN | ✅ | API hostname only (no https://). Baked into nginx proxy_pass. |
VITE_API_BASE | leave empty for Docker path | Empty = browser calls same-origin /admin/* (proxied). |
Add the admin public origin to server CORS_ORIGINS only if the SPA talks cross-origin without the nginx proxy (non-Docker static deploy with VITE_API_BASE=https://chat-api.example.com).
More: Admin panel.
Suggested infrastructure
A single small VM (or container platform) + managed Postgres + S3-compatible object storage covers a pilot. Terminate TLS at Caddy/nginx/Coolify; reserve a stable DNS name for the Intercom webhook.
Edge (required for public launch):
- Terminate TLS and set security headers at the edge.
- Do not expose the Node origin directly; only the proxy may reach it.
- Strip/overwrite client
X-Forwarded-Forat the edge; append the real client hop. - Prefer a WAF / coarse per-IP rate limit in front of
/webhooks/*and/admin/login. The app also applies in-process per-IP limits on/webhooks/*and/files/*(RATE_LIMIT_WEBHOOK_PER_MIN,RATE_LIMIT_FILES_PER_MIN); edge WAF remains recommended.
CI in this repo runs lint → typecheck → test → build on main. How you promote images to your cluster (Coolify, K8s, ECS, …) is up to you.
Scaling beyond one instance
In-memory today: read cache, rate limits, admin lockout, circuit breaker, schedulers (channel publish / retention). For multiple replicas:
- Move cache + rate limits to Redis (same keys).
- Run scheduled publish / retention from one worker (or distributed lock), or posts may publish once per replica.
Hosting these docs
bash
pnpm --filter @aw-chat/docs build # → docs/.vitepress/dist
pnpm --filter @aw-chat/docs previewDeploy the static output to any static host.
Day-2
Backup, rollback, monitoring: Operations.