Removes hardcoded fallback secrets and makes a misconfigured deploy fail loudly instead of silently falling back to development defaults. - Remove insecure JWT fallback secrets (messages.module, configuration) - Remove the 'default-secret' fallback for the 2FA TOTP encryption key and allow a dedicated TWO_FACTOR_ENCRYPTION_KEY so rotating JWT_SECRET no longer locks out every 2FA user (see docs/2fa-key-rotation.md) - Require EMAIL_API_URL; drop the hardcoded vendor email endpoint - Drive WebSocket CORS from CORS_ORIGINS instead of origin:'*' - Load .env before any Nest module is imported (src/load-env.ts). Decorator arguments evaluate at import time, so the gateway previously froze its CORS config to the localhost fallback even when CORS_ORIGINS was set - Add boot-time env validation: missing required vars, weak JWT_SECRET, and inverted access/refresh token lifetimes now abort startup - Enable Redis TLS certificate verification - Require ADMIN_EMAIL/ADMIN_PASSWORD for the seed; remove the published default super-admin credentials and stop printing them - Add the initial Prisma migration and stop gitignoring prisma/migrations - Make .env.example an accurate configuration contract (admin bootstrap, REDIS_TLS, S3_ENDPOINT, 2FA key, Firebase path; drop the dead SMTP block) - Add handover documentation: architecture, ER model, sequence and data-flow diagrams, 2FA key rotation runbook Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.5 KiB
RE:Quest — system architecture
Handover documentation. Diagrams are Mermaid inside Markdown so they are editable and diffable in version control.
1. Components
| Component | Stack | Repo | Notes |
|---|---|---|---|
| API | NestJS (Node), REST + Socket.IO | backend |
Global prefix /api/v1; Swagger at /docs when enabled |
| Web app | Next.js App Router, NextAuth | frontend |
Public site + user and agent portals |
| Admin panel | Next.js App Router | adminpanel |
Internal dashboard, no next/image remote loading |
| Mobile app | Flutter | mobile-app |
Talks to the same REST + Socket.IO API |
| Database | PostgreSQL (DigitalOcean Managed) | — | Prisma ORM, 24 entities |
| Cache / pub-sub | Redis / Valkey (DigitalOcean Managed) | — | Presence tracking + Socket.IO adapter for multi-instance fan-out |
| Object storage | S3-compatible (DigitalOcean Spaces) | — | Browser uploads via presigned URLs; DB stores object keys only |
| Payments | Stripe | — | Checkout Sessions + Billing Portal + webhooks |
| Push | Firebase Cloud Messaging | — | Admin SDK server-side, FCM tokens client-side |
HTTP JSON gateway (EMAIL_API_URL) |
— | No SMTP path exists in the code | |
| Auth (social) | Google via NextAuth | — | Frontend verifies, then posts profile to POST /auth/social |
Component diagram
flowchart TB
subgraph Clients
WEB[Web app<br/>Next.js]
ADM[Admin panel<br/>Next.js]
MOB[Mobile app<br/>Flutter]
end
subgraph API["Backend API — NestJS"]
REST[REST controllers<br/>/api/v1]
WS[Socket.IO gateway<br/>MessagesGateway]
EV[Event emitter<br/>in-process]
end
subgraph Data
PG[(PostgreSQL<br/>Prisma)]
RD[(Redis / Valkey<br/>presence + SIO adapter)]
S3[(Object storage<br/>S3-compatible)]
end
subgraph ThirdParty["Third-party services"]
STR[Stripe]
FCM[Firebase FCM]
MAIL[Email gateway<br/>EMAIL_API_URL]
GOO[Google OAuth]
end
WEB --> REST
ADM --> REST
MOB --> REST
WEB <--> WS
ADM <--> WS
MOB <--> WS
WEB -.NextAuth.-> GOO
WEB -- presigned PUT --> S3
MOB -- presigned PUT --> S3
REST --> PG
REST --> RD
REST --> S3
WS --> PG
WS --> RD
REST --> EV
WS --> EV
EV --> MAIL
EV --> FCM
REST --> STR
STR -- webhook --> REST
Notes on the diagram:
- Files never stream through the API. Clients ask the API for a presigned
URL (
POST /upload/*-presigned-url) and then PUT directly to object storage. Only the resulting object key is persisted. - Redis serves two distinct purposes:
RedisPresenceService(who is online) and the Socket.IO Redis adapter, which is what lets more than one API instance broadcast to the same rooms. Running multiple replicas without Redis would silently break cross-instance message delivery. - Side effects are event-driven. Controllers emit in-process events
(
user.registered,notification.message, …);EmailListenerand the notification service subscribe. Email/push failures are logged, not propagated to the caller.
2. Runtime topology
flowchart LR
U[Browser / device] --> CDN[re-quest.com<br/>admin.re-quest.com]
CDN --> APIH[prod.api.re-quest.com]
APIH --> N1[API instance 1]
APIH --> N2[API instance N]
N1 <--> RD[(Redis / Valkey)]
N2 <--> RD
N1 --> PG[(PostgreSQL)]
N2 --> PG
CORS_ORIGINS must list every client origin. It is consumed twice — by the
HTTP CORS middleware and by the Socket.IO gateway. If it is unset, both
fall back to localhost and every browser request from the live domain is
rejected.
3. Authentication model
| Concern | Mechanism |
|---|---|
| Credential storage | Argon2id hashes (User.password) |
| Session tokens | JWT access + refresh, both signed with JWT_SECRET |
| Access token lifetime | JWT_ACCESS_EXPIRATION (recommended 15m) |
| Refresh token lifetime | JWT_REFRESH_EXPIRATION (recommended 7d, must exceed the access lifetime) |
| Refresh token storage | Session rows in PostgreSQL — revocable via /auth/logout-all |
| Client-side token storage | localStorage (web) — XSS-exfiltratable; migration to httpOnly cookies is an open item |
| Social login | Google only. NextAuth verifies in the frontend and posts the verified profile to POST /auth/social |
| 2FA | TOTP (speakeasy). Secret is AES-256-GCM encrypted at rest with a key derived from TWO_FACTOR_ENCRYPTION_KEY (falling back to JWT_SECRET) |
| WebSocket auth | JWT passed in handshake.auth.token; verified on connect, socket disconnected on failure |
The Facebook and Twitter OAuth variables present in some environments are not consumed by the backend. Google is the only enabled social provider.
4. Roles and authorisation
UserRole is one of USER, AGENT, ADMIN, SUPER_ADMIN. Route access is
enforced by guards on the controllers; the admin panel is a client of the same
API and holds no privileges of its own.
Known gap: agent approval does not verify an active paid subscription on the server. Subscription state is displayed to the admin, but approval is not gated on it in the backend.
5. Configuration contract
backend/.env.example is the authoritative list. Validation runs at boot
(src/config/env.validation.ts) and the process exits if:
DATABASE_URL,JWT_SECRETorEMAIL_API_URLis missingNODE_ENV=productionandCORS_ORIGINSorFRONTEND_URLis missingNODE_ENV=productionandJWT_SECRETis shorter than 32 charactersJWT_REFRESH_EXPIRATIONis not longer thanJWT_ACCESS_EXPIRATION
Warnings (logged, non-fatal): access token lifetime over one hour, REDIS_TLS
disabled in production, sslmode=no-verify in DATABASE_URL, localhost in a
production CORS_ORIGINS.
.env files are loaded by src/load-env.ts, which must remain the first
import of main.ts — see the comment in that file for why.
6. Database change process
The schema is versioned under backend/prisma/migrations. prisma db push
must not be used against any shared environment.
An environment whose schema was created with db push (as production was)
needs baselining once, before the first migrate deploy:
npx prisma migrate resolve --applied 20260721102313_init
npx prisma migrate deploy
Without the baseline, migrate deploy fails with "relation already exists".