Skip to content

Request Lifecycle & Middleware Pipeline

This document is the canonical reference for how an HTTP request travels through the Mr. Mentor / MAS backend (mr-mentor-backend, Express + TypeScript). It traces the request from the moment it hits Express, through the ordered global middleware stack, into per-route authentication and authorization guards, down to the controller, and back out via the JSON response — including the 404 and global error paths. Every middleware is documented with what it checks and what it attaches to req.

Status: documented from source on this branch.


Overview

The backend is a single Express application (src/app.ts) that mounts ~70 route groups (src/routes/index.ts) serving a multi-product EdTech + recruitment suite (Mr. Mentor mentorship, Mr. Hire ATS, MAS LMS, Sales CRM, Finance, AI platform, Vendor API, workflow automation). All of these products share one middleware pipeline and one authentication scheme.

There are three distinct authentication identities in the system:

Identity Credential Attached to req Middleware
Platform user (student, mentor, admin, sales, finance, HR, batch lead, CM, super mentor) JWT (Bearer header or token / authToken cookie) req.user (full User entity) authMiddleware
External vendor (lead-ingestion API consumer) API key (X-API-Key or Authorization: Bearer vk_live_...) req.vendor (VendorApiKey entity) vendorAuthMiddleware
Webhook caller (Exotel, VAPI/voice, Miss Ozone) Shared secret (?token= query or X-Webhook-Secret header) nothing — validated inline in the handler per-route guard function

Roles live in src/types/UserTypes.ts: USER, ADMIN, FINANCE, EXPERT, SALES, SALES_HEAD, EXTERNAL_HR, BATCH_LEAD, SUPER_MENTOR, COMMUNITY_MANAGER. ADMIN is the universal-access role — every role guard except vendorAuth and the webhook guards lets ADMIN through. The former SUPERADMIN role has been removed; existing superadmin rows are migrated to admin at startup (see src/types/UserTypes.ts comment).

Cross-link: Identity & Access · Security Architecture


Key concepts & entities

Concept Meaning
Global middleware Runs on every request, registered in App.initializeMiddlewares() (src/app.ts). Order: CORS → Helmet → JSON body parse → urlencoded body parse → static /assets.
Route-level middleware Auth + role guards applied per route or per router (router.use(...)). Not global.
AuthenticatedRequest Request & { user?: User } — the request shape after authMiddleware runs (src/middleware/auth.middleware.ts).
VendorAuthenticatedRequest Request & { vendor?: VendorApiKey } (src/middleware/vendorAuth.middleware.ts).
Role gate A small middleware that checks req.user.role and 403s if it is not in the allowed set. ADMIN bypasses all of them.
Fail-open totpRateLimit calls next() even if Redis is down so a Redis outage does not lock out logins.
Logout-everywhere / session revoke authMiddleware re-validates the JWT against the DB on every request, rejecting tokens issued before a password change or bound to a revoked session.

Source files (entities & middleware)

File Role
src/middleware/auth.middleware.ts JWT verification → req.user; password-change + session-revoke checks
src/middleware/admin.middleware.ts adminMiddleware — ADMIN-only
src/middleware/expert.middleware.ts expertMiddleware — EXPERT or ADMIN
src/middleware/finance.middleware.ts financeOnlyMiddleware — FINANCE or ADMIN
src/middleware/hrRole.middleware.ts hrRoleMiddleware — EXTERNAL_HR or ADMIN
src/middleware/batchLead.middleware.ts batchLeadMiddleware, adminOrBatchLeadMiddleware — BATCH_LEAD or ADMIN
src/middleware/salesHeadPage.middleware.ts requireHeadPagePermission(...pages) — per-page sales-head flags
src/middleware/isEnrolled.middleware.ts isEnrolledMiddleware — enrolled-student gate (DB lookup)
src/middleware/totpRateLimit.middleware.ts totpRateLimit — Redis IP rate limit (5 / 15 min)
src/middleware/vendorAuth.middleware.ts vendorAuthMiddleware(perm)req.vendor + async access logging
src/entities/User.ts The platform user (role, isActive, passwordChangedAt, salesHeadId)
src/entities/RefreshToken.ts Session record used for per-session revoke (sid)
src/entities/VendorApiKey.ts External vendor API key
src/entities/VendorApiKeyAccessSummary.ts Aggregated vendor access log

Note on financeAudit: there is no financeAudit middleware. Finance audit is recorded at the service layer (GstAuditLog entity + GlobalPaymentSyncService), not as a request-pipeline middleware. It is documented in the Finance domain doc.


Architecture

flowchart TD
    Client["Client (browser / mobile / vendor / webhook)"] --> Express["Express app (src/app.ts)"]

    subgraph GLOBAL["Global middleware (every request, in order)"]
        direction TB
        CORS["1. CORS (origin allowlist, credentials true)"]
        HELMET["2. Helmet (crossOriginResourcePolicy cross-origin)"]
        JSON["3. express.json limit 20mb"]
        URL["4. express.urlencoded limit 20mb"]
        STATIC["5. /assets static files"]
        CORS --> HELMET --> JSON --> URL --> STATIC
    end

    Express --> GLOBAL
    GLOBAL --> ROUTER["Routes.router (src/routes/index.ts)"]

    ROUTER --> OPT["OPTIONS * preflight handler"]
    ROUTER --> MATCH{"Path matches a mounted router?"}

    MATCH -->|no| NF["404 handler: app.use star"]
    MATCH -->|yes| GUARDS["Route-level guards"]

    subgraph GUARDS["Per-route auth + role guards"]
        direction TB
        AUTH["authMiddleware -> req.user"]
        VAUTH["vendorAuthMiddleware -> req.vendor"]
        WHGUARD["webhook token guard"]
        ROLE["role gate: admin / expert / finance / hr / batchLead / salesHead / isEnrolled"]
    end

    GUARDS --> CTRL["Controller (src/controllers/*)"]
    CTRL --> SVC["Service (src/services/*)"]
    SVC --> DB[("PostgreSQL via TypeORM")]
    SVC --> REDIS[("Redis / BullMQ queues")]
    SVC --> EXT["External: Razorpay, S3, Exotel, OpenAI, Google"]

    CTRL -->|throws| ERR["Global error handler -> 500"]
    CTRL --> RESP["JSON response to client"]
    NF --> RESP
    ERR --> RESP

The pipeline is strictly ordered. CORS is registered first (before Helmet) on purpose — the comment in src/app.ts notes CORS "MUST be first, before helmet and other middleware" so that cross-origin preflights succeed even when Helmet sets restrictive headers.


The global middleware pipeline (ordered)

All five run on every request, registered in App.initializeMiddlewares() (src/app.ts lines 35–97).

1. CORS (cors)

  • Origin check is a function: requests with no Origin (curl, mobile, server-to-server) are allowed; an Origin: null (file:// pages) is allowed; otherwise the origin must be in allowedOrigins, unless NODE_ENV === "development" in which case all origins pass.
  • allowedOrigins = a hard-coded defaultOrigins list (Dron Engineering + IIMT Space-Tech Vercel deployments) merged with JSON.parse(process.env.CORS_ALLOWED_ORIGINS) when that env var is set.
  • credentials: true (cookies allowed). Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS.
  • Allowed request headers: Content-Type, Authorization, X-Requested-With, X-Platform, X-Webhook-Secret.
  • Exposed headers: Content-Range, X-Content-Range. Preflight cache maxAge: 86400 (24h).
  • A rejected origin gets new Error("CORS not allowed"), which falls through to the global error handler (500).

Gotcha: in development the allowlist is bypassed entirely. In production the only browser origins that work are the hard-coded Vercel hosts plus whatever CORS_ALLOWED_ORIGINS contains (the admin/student frontends are added there).

2. Helmet (helmet)

  • Configured with crossOriginResourcePolicy: { policy: "cross-origin" } so static /assets (logos, banner images) load cross-origin. All other Helmet defaults (HSTS, noSniff, frameguard, etc.) apply.

3. express.json({ limit: "20mb" })

  • Parses application/json bodies up to 20 MB. Large payloads (bulk-upload JSON, base64 banner assets) need this raised limit. Exceeding it produces a body-parser error → global error handler.

4. express.urlencoded({ extended: true, limit: "20mb" })

  • Parses form-encoded bodies up to 20 MB. Notably this lets the Exotel webhook receive form-encoded status callbacks (Exotel posts either JSON or form data; req.body covers both).

5. /assets static files

  • express.static('public/assets') serves logos/images. This is the only static mount.

Not enabled: morgan request logging is imported but commented out (// this.app.use(morgan('combined')) at src/app.ts:89). trust proxy is NOT configured — this is why vendorAuth and totpRateLimit read X-Forwarded-For manually to find the real client IP.

After the global stack, Routes.router is mounted at / (this.app.use("/", this.routes.router)).


Per-middleware reference (route-level guards)

Each of these is applied per route or per sub-router, after the global stack. They assume authMiddleware has already populated req.user (except vendorAuth and the webhook guards, which carry their own identity).

Middleware Attaches / checks Pass condition Failure
authMiddleware sets req.user (full User from DB) valid JWT + user exists + isActive + token not predating password change + session not revoked 401 missing/invalid/expired, 403 blocked, 500 if JWT_SECRET unset
adminMiddleware reads req.user.role role === ADMIN 403
expertMiddleware reads req.user.role ADMIN (bypass) or EXPERT 403
financeOnlyMiddleware reads req.user.role FINANCE or ADMIN 401 if no user, 403 otherwise
hrRoleMiddleware reads req.user.role EXTERNAL_HR or ADMIN 403
batchLeadMiddleware / adminOrBatchLeadMiddleware reads req.user.role BATCH_LEAD or ADMIN 401 if no user, 403 otherwise
requireHeadPagePermission(...pages) loads sales-head profile flags non-SALES_HEAD pass through; SALES_HEAD must hold ANY listed page flag 401 if no user, 403 if no flag, 500 on lookup error
isEnrolledMiddleware DB lookup on Application ADMIN/COMMUNITY_MANAGER bypass; else an Application with status ENROLLED/BATCH_ALLOCATED/PAID 401 if no user, 403 if not enrolled, 500 on error
totpRateLimit Redis counter totp_rate:<ip> ≤ 5 attempts / 15 min 429; fails open if Redis down
vendorAuthMiddleware(perm) sets req.vendor (VendorApiKey) valid key + status ACTIVE + permission (submit/read) 401 missing/invalid, 403 disabled/revoked/no-permission, 500 on error

authMiddleware — the heart of the pipeline (src/middleware/auth.middleware.ts)

  1. Extract token from Authorization: Bearer <token>, or cookie token, or cookie authToken. Missing → 401 "Authorization token missing".
  2. If JWT_SECRET env is unset → 500 "Server configuration error".
  3. jwt.verify(token, secret, { algorithms: ['HS256'] }) → decodes { id, email, role, iat, sid }. Expired token → 401 TOKEN_EXPIRED; malformed → 401 INVALID_TOKEN.
  4. DB re-validation — loads the User row (id, email, role, isActive, isVerified, salesHeadId, passwordChangedAt).
  5. User not found → 401 "User no longer exists".
  6. !isActive403 "Your account has been blocked".
  7. Logout-everywhere: if passwordChangedAt is set and the token's iat (seconds) is before the password change second → 401 PASSWORD_CHANGED. (Same-second re-login is allowed.)
  8. Per-session revoke: if the token carries a sid, load the matching RefreshToken. If that session is revoked and has no replacedByToken (i.e. an explicit logout/revoke, not normal rotation) → 401 SESSION_REVOKED. Missing sessions and rotated sessions are left alone.
  9. On success, attaches the real DB User object (not the JWT claims) to req.user and calls next().

This is why role can never be spoofed by editing a JWT claim: the role used by every downstream guard comes from the DB row loaded in step 4, not from the token.

vendorAuthMiddleware — external API identity (src/middleware/vendorAuth.middleware.ts)

  • Reads key from X-API-Key or Authorization: Bearer <key>; expects format vk_live_xxxx.yyyy (prefix + secret).
  • Looks up VendorApiKey by keyPrefix, then does a constant-time hash compare of the secret (runs the compare even when the prefix is missing to avoid a timing oracle).
  • Enforces status === ACTIVE and the required permission (canSubmit / canRead).
  • Sets req.vendor and registers a res.on('finish') hook that, via setImmediate, asynchronously upserts a vendor_api_key_access_summaries row (hit count, last status, latency) and bumps VendorApiKey.lastUsedAt/requestCount. Logging never blocks the response.

Authorization decision flow

flowchart TD
    REQ["Incoming request to a mounted route"] --> KIND{"Which identity does the route use?"}

    KIND -->|"Vendor API /api/v1/vendor"| V1["vendorAuthMiddleware(perm)"]
    V1 --> VOK{"Key valid, ACTIVE, has perm?"}
    VOK -->|no| V403["401 or 403"]
    VOK -->|yes| VCTRL["req.vendor set -> controller"]

    KIND -->|"Webhook /api/exotel, voice, miss-ozone"| W1["token guard"]
    W1 --> WOK{"Shared secret matches?"}
    WOK -->|no| W403["403 Invalid webhook token"]
    WOK -->|yes| WCTRL["controller"]

    KIND -->|"Public (health, candidate-quiz, judge0, apply, public courses)"| PCTRL["controller (no auth)"]

    KIND -->|"Platform user"| A1["authMiddleware"]
    A1 --> AOK{"JWT valid, user active, session live?"}
    AOK -->|no| A401["401 / 403"]
    AOK -->|yes| ADMINBP{"req.user.role == ADMIN?"}

    ADMINBP -->|yes| UCTRL["controller (ADMIN bypasses role gates)"]
    ADMINBP -->|no| GATE{"Route role gate"}

    GATE -->|"/api/admin/*"| GADMIN["adminMiddleware -> ADMIN only -> 403"]
    GATE -->|"/api/finance/*"| GFIN["financeOnlyMiddleware -> FINANCE"]
    GATE -->|"expert routes"| GEXP["expertMiddleware -> EXPERT"]
    GATE -->|"HR routes"| GHR["hrRoleMiddleware -> EXTERNAL_HR"]
    GATE -->|"/api/batchlead/*"| GBL["batchLeadMiddleware -> BATCH_LEAD"]
    GATE -->|"sales-head pages"| GSH["requireHeadPagePermission -> page flag"]
    GATE -->|"enrolled student routes"| GEN["isEnrolledMiddleware -> Application status"]
    GATE -->|"plain authed routes"| GANY["any authenticated user"]

    GADMIN --> UCTRL
    GFIN --> UCTRL
    GEXP --> UCTRL
    GHR --> UCTRL
    GBL --> UCTRL
    GSH --> UCTRL
    GEN --> UCTRL
    GANY --> UCTRL

Which chain protects which route group

Derived from src/routes/index.ts mounts and the individual route files:

Mount prefix Typical guard chain Notes
/api/health, /api/test-data none public liveness
/api/auth/* none (login), totpRateLimit on /auth/master-totp issues JWTs
/api/admin/* (+ /api/admin/vendor-keys, /api/admin/new-courses, job-applications, resume-analysis, all-responses) authMiddleware + adminMiddleware ADMIN only
/api/finance/* authMiddleware + financeOnlyMiddleware (applied router-wide via router.use) FINANCE or ADMIN
/api/v1/vendor/* vendorAuthMiddleware('submit'|'read') API-key identity, no req.user
/api/exotel/* per-route ?token= shared-secret guard public webhook
/api/batchlead/* authMiddleware + batchLeadMiddleware (router-wide) except /upload-score which is unauthenticated
/api/student/* authMiddleware (+ isEnrolledMiddleware on paid resources) dashboard handles both enrolled + non-enrolled
/api/candidate-quiz/* mostly public (token is the auth); authMiddleware on admin quiz-results public quiz delivery
/api/judge0/* public code-runner proxy called from quiz pages
Sales/CRM (/api/sales, raw-leads, campaign-leads) authMiddleware + role/requireHeadPagePermission sales-head per-page flags
Mr. Hire HR endpoints authMiddleware + hrRoleMiddleware EXTERNAL_HR or ADMIN
Voice interview / Miss Ozone webhooks X-Webhook-Secret header validated in controller public callback

API surface (the pipeline's own endpoints)

The middleware pipeline itself does not own a feature API, but src/app.ts and src/routes/index.ts register a handful of infrastructural endpoints that exercise the pipeline directly:

Method Path Auth/role Purpose
GET / none API banner / version JSON (src/app.ts)
OPTIONS * none CORS preflight responder, returns 204 (src/app.ts)
GET /api/health none Health check (HealthRoutes)
GET /api/test-data none DB connectivity debug probe (src/routes/index.ts)
GET /auth/google/callback none Redirect shim → /api/google-calendar/callback
* /admin/queues Bull Board UI BullMQ monitoring (BullBoardRoutes)
POST /api/auth/master-totp totpRateLimit TOTP verify, rate-limited 5/15min per IP
POST /api/v1/vendor/leads vendorAuthMiddleware('submit') vendor lead submit
GET /api/v1/vendor/leads vendorAuthMiddleware('read') vendor lead list
GET /api/v1/vendor/leads/:id vendorAuthMiddleware('read') vendor lead fetch
POST /api/exotel/call-status ?token= shared secret Exotel call status webhook
POST /api/exotel/sms-status ?token= shared secret Exotel SMS status webhook
* (unmatched) none 404 JSON { success:false, message:"Route not found", path }

User journeys

Journey 1 — Authenticated platform request (admin endpoint)

The canonical happy path: a logged-in admin calls a protected /api/admin/* endpoint.

sequenceDiagram
    participant FE as Frontend
    participant EX as Express global stack
    participant AUTH as authMiddleware
    participant DB as PostgreSQL
    participant ROLE as adminMiddleware
    participant CTRL as Controller

    FE->>EX: GET /api/admin/users with Bearer token
    EX->>EX: CORS check then Helmet then body parse
    EX->>AUTH: pass request
    AUTH->>AUTH: extract token from header or cookie
    AUTH->>AUTH: jwt.verify with HS256
    AUTH->>DB: load User by decoded id
    DB-->>AUTH: user row with role and isActive
    Note over AUTH: check isActive, passwordChangedAt, session sid
    AUTH->>AUTH: set req.user to DB user
    AUTH->>ROLE: next
    ROLE->>ROLE: req.user.role equals ADMIN
    ROLE->>CTRL: next
    CTRL->>DB: query data
    DB-->>CTRL: rows
    CTRL-->>FE: 200 JSON payload

Alternate — token expired: jwt.verify throws TokenExpiredError → 401 with errorType: "TOKEN_EXPIRED"; the frontend silently refreshes and retries.

Alternate — blocked account: DB row has isActive=false → 403 "Your account has been blocked".

Alternate — wrong role: a USER hits /api/admin/*authMiddleware passes, adminMiddleware returns 403 "Access denied. Admin role required."

Journey 2 — Logout-everywhere / session revoke

A user changes their password (or an admin revokes a session). Old access tokens are still cryptographically valid until expiry, so authMiddleware re-checks them against the DB on every request.

sequenceDiagram
    participant OLD as Old device
    participant AUTH as authMiddleware
    participant DB as PostgreSQL

    Note over OLD: still holds a token minted before password change
    OLD->>AUTH: GET /api/student/dashboard with old token
    AUTH->>AUTH: jwt.verify succeeds, token not yet expired
    AUTH->>DB: load User including passwordChangedAt
    DB-->>AUTH: passwordChangedAt is after token iat
    AUTH-->>OLD: 401 reason PASSWORD_CHANGED
    Note over OLD: frontend forces re-login

    OLD->>AUTH: retry with token carrying revoked sid
    AUTH->>DB: load RefreshToken by sid
    DB-->>AUTH: session revoked and no replacedByToken
    AUTH-->>OLD: 401 reason SESSION_REVOKED

Journey 3 — External vendor lead submission (API key)

A third-party vendor posts a lead using an API key. No req.user; identity is the VendorApiKey. Access logging is fire-and-forget.

sequenceDiagram
    participant VEND as Vendor system
    participant VAUTH as vendorAuthMiddleware submit
    participant DB as PostgreSQL
    participant CTRL as VendorLeadController

    VEND->>VAUTH: POST /api/v1/vendor/leads with X-API-Key vk_live_aaa.bbb
    VAUTH->>VAUTH: parse key into prefix and secret
    VAUTH->>DB: find VendorApiKey by keyPrefix
    DB-->>VAUTH: record with keyHash and status
    VAUTH->>VAUTH: constant-time hash compare
    alt key invalid
        VAUTH-->>VEND: 401 Invalid API key
    else status not ACTIVE
        VAUTH-->>VEND: 403 disabled or revoked
    else missing submit permission
        VAUTH-->>VEND: 403 no submit permission
    else valid
        VAUTH->>VAUTH: set req.vendor
        VAUTH->>CTRL: next
        CTRL->>DB: insert lead
        DB-->>CTRL: saved
        CTRL-->>VEND: 201 created
        Note over VAUTH: on response finish, async upsert access summary and bump request count
    end

Journey 4 — Public webhook (Exotel call status)

Telephony status callbacks arrive unauthenticated but are protected by a shared-secret query token that ExotelService appended when registering the callback URL.

sequenceDiagram
    participant EXO as Exotel
    participant EX as Express global stack
    participant GUARD as webhook token guard
    participant SVC as ExotelLeadService
    participant DB as PostgreSQL

    EXO->>EX: POST /api/exotel/call-status?token=secret form or json body
    EX->>EX: urlencoded and json parse both handled
    EX->>GUARD: pass request
    GUARD->>GUARD: ExotelService.verifyWebhookToken on query token
    alt token mismatch
        GUARD-->>EXO: 403 Invalid webhook token
    else token ok
        GUARD->>SVC: handleCallStatus with body
        SVC->>DB: update lead call activity
        DB-->>SVC: done
        SVC-->>EXO: 200 success true
        Note over SVC: even on internal error returns 200 so Exotel does not retry forever
    end

Journey 5 — Enrolled-student gate

Paid student resources require not just a valid login but an active enrollment.

sequenceDiagram
    participant FE as Student frontend
    participant AUTH as authMiddleware
    participant ENR as isEnrolledMiddleware
    participant DB as PostgreSQL
    participant CTRL as StudentController

    FE->>AUTH: GET protected student resource with token
    AUTH->>DB: load user
    DB-->>AUTH: user role USER
    AUTH->>ENR: next with req.user set
    alt role is ADMIN or COMMUNITY_MANAGER
        ENR->>CTRL: bypass and next
    else
        ENR->>DB: find Application with status ENROLLED or BATCH_ALLOCATED or PAID
        DB-->>ENR: application found or null
        alt no active application
            ENR-->>FE: 403 must be an officially enrolled student
        else found
            ENR->>CTRL: next
            CTRL-->>FE: 200 data
        end
    end

Journey 6 — Sales-head per-page permission

A sales head only sees the CRM/Sales pages their profile flags allow. Non-head roles fall straight through to the route's own role guard.

sequenceDiagram
    participant FE as Sales frontend
    participant AUTH as authMiddleware
    participant PAGE as requireHeadPagePermission crm
    participant SVC as SalesHeadProfileService
    participant CTRL as Controller

    FE->>AUTH: GET /api/sales/raw-leads with token
    AUTH->>PAGE: next with req.user set
    alt role is not SALES_HEAD
        PAGE->>CTRL: pass through, route role guard decides
    else role is SALES_HEAD
        PAGE->>SVC: getEffectivePermissions for user
        SVC-->>PAGE: permission flags
        alt holds canAccessCrm flag
            PAGE->>CTRL: next
            CTRL-->>FE: 200 data
        else missing flag
            PAGE-->>FE: 403 no access to this section
        end
    end

Journey 7 — 404 and global error

sequenceDiagram
    participant FE as Client
    participant ROUTER as Routes.router
    participant NF as 404 handler
    participant CTRL as Controller
    participant ERR as Global error handler

    FE->>ROUTER: GET /api/does-not-exist
    ROUTER->>NF: no route matched, app.use star
    NF-->>FE: 404 success false message Route not found path echoed

    FE->>ROUTER: GET /api/something that throws
    ROUTER->>CTRL: matched route
    CTRL->>CTRL: throws or calls next with error
    CTRL->>ERR: error propagates
    ERR->>ERR: console.error then 500
    Note over ERR: error.message included only when NODE_ENV is development
    ERR-->>FE: 500 success false message Internal Server Error

Background jobs & async

The request pipeline itself spawns no jobs, but two middleware-adjacent async patterns matter:

  • Vendor access logging (vendorAuthMiddleware): a res.on('finish') + setImmediate block performs an atomic upsert into vendor_api_key_access_summaries and updates the key's counters after the response is sent, so it never adds latency.
  • TOTP rate counter (totpRateLimit): writes a Redis key totp_rate:<ip> with a 15-minute TTL set on first attempt. Redis is the BullMQ broker too (src/config/redis.ts).

BullMQ queues/workers (email, database, cleanup, kpi, resumeAnalysis) are unrelated to the inbound middleware chain — see Background Jobs and the root CLAUDE.md worker table.


External integrations

Integration Touched by pipeline Env vars Failure / fallback
JWT authMiddleware JWT_SECRET unset → 500 "Server configuration error" on every authed request
PostgreSQL authMiddleware, isEnrolledMiddleware, requireHeadPagePermission, vendorAuthMiddleware DB_* DB error in isEnrolled/requireHeadPagePermission → 500; authMiddleware DB error → caught by the verify try/catch → 401
Redis totpRateLimit REDIS_HOST, REDIS_PORT fails opennext() on error so logins are never blocked by a Redis outage
Exotel webhook token guard EXOTEL_WEBHOOK_TOKEN empty/mismatched token → 403; feature self-disables when Exotel env is unset
VAPI / Voice / Miss Ozone X-Webhook-Secret header check (in controllers) per-service secret mismatch → 401/403 in the handler

CORS allowed origins (exact)

Hard-coded defaults in src/app.ts:

  • https://dron-engineering.vercel.app
  • https://dron-engineering-gamma.vercel.app
  • https://iimt-space-tech.vercel.app
  • https://iimt-space-tech-rohan-pandeys-projects-a9888392.vercel.app
  • https://iimt-space-tech-rohanpandeydev-rohan-pandeys-projects-a9888392.vercel.app

…plus every entry in JSON.parse(process.env.CORS_ALLOWED_ORIGINS). In NODE_ENV=development, all origins are accepted.

Body-size limits

20mb for both JSON and urlencoded bodies (src/app.ts:92–93).


Status lifecycles

The pipeline gates on two entities whose status fields drive authorization decisions.

VendorApiKey status (src/entities/VendorApiKey.ts)

stateDiagram-v2
    [*] --> ACTIVE: key created
    ACTIVE --> DISABLED: admin disables
    DISABLED --> ACTIVE: admin re-enables
    ACTIVE --> REVOKED: admin revokes
    DISABLED --> REVOKED: admin revokes
    REVOKED --> [*]
    note right of ACTIVE
        only ACTIVE keys authenticate
        DISABLED and REVOKED return 403
    end note

Application enrollment status (gate used by isEnrolledMiddleware)

stateDiagram-v2
    [*] --> APPLIED
    APPLIED --> PAID: payment received
    PAID --> BATCH_ALLOCATED: assigned to a batch
    BATCH_ALLOCATED --> ENROLLED: enrollment finalized
    note right of PAID
        PAID, BATCH_ALLOCATED and ENROLLED
        all satisfy isEnrolledMiddleware
        earlier states are forbidden
    end note

Edge cases, limits & gotchas

  • ADMIN is god-mode. Every role guard except vendorAuth and webhook guards lets ADMIN through (expertMiddleware, isEnrolledMiddleware, etc. all early-return for ADMIN). There is no SUPERADMIN anymore — those rows are migrated to ADMIN at startup.
  • Role comes from the DB, not the JWT. authMiddleware reloads the user every request, so demoting/blocking a user takes effect immediately, and a tampered JWT role claim is ignored.
  • trust proxy is not set. req.ip is unreliable behind a proxy; vendorAuth and totpRateLimit read X-Forwarded-For / cf-connecting-ip / x-real-ip manually. If you add IP-based logic, do the same.
  • CORS dev bypass. NODE_ENV=development accepts any origin — never run production with NODE_ENV=development.
  • No-origin requests are allowed. curl, mobile apps, and server-to-server calls (no Origin header) always pass CORS — they are gated only by auth.
  • totpRateLimit fails open. A Redis outage disables the rate limit rather than locking everyone out. Acceptable for availability but means the limit is best-effort.
  • Legacy tokens with no sid skip the session-revoke check; they age out within the 15-minute access-token lifetime and pick up a sid on the next refresh.
  • Token rotation vs. revoke. A RefreshToken revoked with a replacedByToken (normal rotation) does NOT trigger logout — only an explicit revoke without replacement does. This prevents spurious logouts during the admin frontend's proactive refresh.
  • Webhooks always 200 on internal errors. Exotel handlers return 200 even when processing fails, to stop infinite provider retries. Monitoring of webhook health must look at logs, not status codes.
  • morgan logging is off. Request logging is commented out; authMiddleware does its own console.debug per request instead.
  • 404 is JSON, not HTML. Unmatched routes return { success:false, message:"Route not found", path }. The global error handler always returns a generic 500 and only leaks error.message when NODE_ENV=development.
  • X-Platform header (multi-tenant). CORS allows X-Platform, but no middleware reads it — multi-tenant routing is handled inside specific controllers (e.g. auth.controller.ts, issues.controller.ts), not the pipeline. (inferred from header allowlist + controller usage)
  • financeAudit is not a middleware. Audit trails for finance are written at the service layer (GstAuditLog), not in the request pipeline.