Files
backend/docs/architecture.md
Sathish c9b38dc6ab fix(security): resolve audit findings — secrets, env contract, migrations
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>
2026-08-04 16:03:08 +05:30

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
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

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

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:

npx prisma migrate resolve --applied 20260721102313_init
npx prisma migrate deploy

Without the baseline, migrate deploy fails with "relation already exists".