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>
182 lines
6.5 KiB
Markdown
182 lines
6.5 KiB
Markdown
# RE:Quest — system architecture
|
|
|
|
Handover documentation. Diagrams are Mermaid inside Markdown so they are
|
|
editable and diffable in version control.
|
|
|
|
- [Data model / ERD](./data-model.md)
|
|
- [Workflow (sequence) diagrams](./flows.md)
|
|
- [2FA key rotation runbook](./2fa-key-rotation.md)
|
|
|
|
---
|
|
|
|
## 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 |
|
|
| Email | 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
|
|
|
|
```mermaid
|
|
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`, …); `EmailListener` and the
|
|
notification service subscribe. Email/push failures are logged, not
|
|
propagated to the caller.
|
|
|
|
---
|
|
|
|
## 2. Runtime topology
|
|
|
|
```mermaid
|
|
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_SECRET` or `EMAIL_API_URL` is missing
|
|
- `NODE_ENV=production` and `CORS_ORIGINS` or `FRONTEND_URL` is missing
|
|
- `NODE_ENV=production` and `JWT_SECRET` is shorter than 32 characters
|
|
- `JWT_REFRESH_EXPIRATION` is not longer than `JWT_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`:
|
|
|
|
```bash
|
|
npx prisma migrate resolve --applied 20260721102313_init
|
|
npx prisma migrate deploy
|
|
```
|
|
|
|
Without the baseline, `migrate deploy` fails with "relation already exists".
|