# 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
Next.js] ADM[Admin panel
Next.js] MOB[Mobile app
Flutter] end subgraph API["Backend API — NestJS"] REST[REST controllers
/api/v1] WS[Socket.IO gateway
MessagesGateway] EV[Event emitter
in-process] end subgraph Data PG[(PostgreSQL
Prisma)] RD[(Redis / Valkey
presence + SIO adapter)] S3[(Object storage
S3-compatible)] end subgraph ThirdParty["Third-party services"] STR[Stripe] FCM[Firebase FCM] MAIL[Email gateway
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
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".