Appearance
Security
Handoff checklist for platform and security reviewers. Implementation lives under apps/server/src/ (middleware, routes, env.ts).
Production boot guards
When NODE_ENV=production, the server refuses to start unless:
| Requirement | Why |
|---|---|
REQUIRE_JWT_EXP=1 | Reject session JWTs without exp |
JWT_AUD set | Reject tokens without the expected audience (e.g. aw-chat) |
JWT_ISS set | Reject tokens without the expected issuer |
REQUIRE_S3=1 | Durable uploads (no ephemeral container disk) |
Non-empty CORS_ORIGINS | Explicit browser allowlist |
JWT_SECRET ≥ 32 chars | Client session signing |
ADMIN_SESSION_SECRET ≥ 32, ≠ JWT | Admin session signing |
FILE_URL_SECRET ≥ 32, distinct | Public file URL HMAC |
No custom INTERCOM_BASE_URL | Prevents token exfiltration via API override |
See assertProductionSecurity() in apps/server/src/env.ts.
Opt-in hardening (default off, so enabling can't surprise a not-yet-ready deploy): REQUIRE_DB_VERIFY_FULL=1 asserts sslmode=verify-full, REQUIRE_S3_HTTPS=1 asserts an https S3 endpoint. Plus JWT_MAX_AGE, SKIP_MIGRATIONS, REQUIRE_MEDIA_HTTPS / SAFE_MEDIA_HOSTS, and DEVTOOLS_RESET_TOKEN. Full list and the review they close: Server review remediation.
Client auth (SDK)
- HS256 JWT in
Authorization: Bearer. - Claim:
userIdorsub; production also requiresexp, and when configuredaud/iss(JWT_AUD/JWT_ISS). Optionaljtiis accepted (no revoke store in this service — short TTL + host mint controls). - Mint only on the host backend with the shared
JWT_SECRET. - Resource routes check the contact owns the conversation / ticket / file (IDOR → 403). CSAT/complaint link ids and device DELETE are covered the same way. Covered by server tests.
Host mint contract (required outside this repo)
Sign HS256 with the same secret as chat JWT_SECRET. Claims:
| Claim | Value |
|---|---|
userId or sub | Host user id |
exp | Required (TTL ≤ 1h recommended) |
aud | Must match chat JWT_AUD (e.g. aw-chat) |
iss | Must match chat JWT_ISS |
Chat host env: REQUIRE_JWT_EXP=1, JWT_AUD, JWT_ISS. Full AW / rn-app / infra checklist: SEC-48 handoff. Edge notes also in Deployment.
Admin auth
- Password verified against
admin_users(scrypt). - Session = HMAC token (
ADMIN_SESSION_SECRET); deactivated users fail verification. - Login lockout by IP (in-memory) — configure
TRUST_PROXY_HOPSbehind proxies. - Permission gates on mutating admin routes.
Files
- Upload allow-list by magic bytes (JPEG/PNG/GIF/WEBP/BMP/PDF/MP4/TXT), size caps (
MAX_FILE_MB/ image / video). - Authenticated
GET /api/files/:idenforces ownership. - Public
GET /files/:id?t=requires a key-bound HMAC (FILE_URL_SECRET). Treat leaked URLs as temporary bearers.
Webhooks
POST /webhooks/intercomverifiesX-Hub-Signature(SHA1) withINTERCOM_CLIENT_SECRET.- Events deduped in
processed_events.
Content safety
- Operator HTML → typed rich-text tree in the SDK (no
dangerouslySetInnerHTMLin chat UI); URL schemes allow-listed. - Channel comments: no links, DLP (seed / key / card / password) with redacted moderation events.
Devtools
/api/devtools (contact wipe) mounts only when ENABLE_DEVTOOLS=1 andNODE_ENV !== "production". Bulk wipe also needs ENABLE_DEVTOOLS_RESET_ALL=1. Leave both empty on shared stands and production.
Secrets rotation
| Secret | Effect of rotate |
|---|---|
JWT_SECRET | All client session tokens invalid |
ADMIN_SESSION_SECRET | All admin sessions invalid |
FILE_URL_SECRET | Existing public file links fail verify |
| Intercom / S3 / FCM / Telegram | Update env + redeploy; rotate at provider |
Logging
Message bodies, raw files, and seed-like secrets must not appear in server logs. Diagnostics accept device metadata only.
Related docs
- Configuration — env reference
- Data model — where PII lands
- Operations — backup / incident notes