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 nofinanceAuditmiddleware. Finance audit is recorded at the service layer (GstAuditLogentity +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; anOrigin: null(file:// pages) is allowed; otherwise the origin must be inallowedOrigins, unlessNODE_ENV === "development"in which case all origins pass. allowedOrigins= a hard-codeddefaultOriginslist (Dron Engineering + IIMT Space-Tech Vercel deployments) merged withJSON.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 cachemaxAge: 86400(24h). - A rejected origin gets
new Error("CORS not allowed"), which falls through to the global error handler (500).
Gotcha: in
developmentthe allowlist is bypassed entirely. In production the only browser origins that work are the hard-coded Vercel hosts plus whateverCORS_ALLOWED_ORIGINScontains (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/jsonbodies 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.bodycovers 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)¶
- Extract token from
Authorization: Bearer <token>, or cookietoken, or cookieauthToken. Missing → 401 "Authorization token missing". - If
JWT_SECRETenv is unset → 500 "Server configuration error". jwt.verify(token, secret, { algorithms: ['HS256'] })→ decodes{ id, email, role, iat, sid }. Expired token → 401TOKEN_EXPIRED; malformed → 401INVALID_TOKEN.- DB re-validation — loads the
Userrow (id, email, role, isActive, isVerified, salesHeadId, passwordChangedAt). - User not found → 401 "User no longer exists".
!isActive→ 403 "Your account has been blocked".- Logout-everywhere: if
passwordChangedAtis set and the token'siat(seconds) is before the password change second → 401PASSWORD_CHANGED. (Same-second re-login is allowed.) - Per-session revoke: if the token carries a
sid, load the matchingRefreshToken. If that session isrevokedand has noreplacedByToken(i.e. an explicit logout/revoke, not normal rotation) → 401SESSION_REVOKED. Missing sessions and rotated sessions are left alone. - On success, attaches the real DB
Userobject (not the JWT claims) toreq.userand callsnext().
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-KeyorAuthorization: Bearer <key>; expects formatvk_live_xxxx.yyyy(prefix + secret). - Looks up
VendorApiKeybykeyPrefix, 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 === ACTIVEand the required permission (canSubmit/canRead). - Sets
req.vendorand registers ares.on('finish')hook that, viasetImmediate, asynchronously upserts avendor_api_key_access_summariesrow (hit count, last status, latency) and bumpsVendorApiKey.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): ares.on('finish')+setImmediateblock performs an atomic upsert intovendor_api_key_access_summariesand updates the key's counters after the response is sent, so it never adds latency. - TOTP rate counter (
totpRateLimit): writes a Redis keytotp_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 open — next() 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.apphttps://dron-engineering-gamma.vercel.apphttps://iimt-space-tech.vercel.apphttps://iimt-space-tech-rohan-pandeys-projects-a9888392.vercel.apphttps://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
vendorAuthand webhook guards letsADMINthrough (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.
authMiddlewarereloads the user every request, so demoting/blocking a user takes effect immediately, and a tampered JWTroleclaim is ignored. trust proxyis not set.req.ipis unreliable behind a proxy;vendorAuthandtotpRateLimitreadX-Forwarded-For/cf-connecting-ip/x-real-ipmanually. If you add IP-based logic, do the same.- CORS dev bypass.
NODE_ENV=developmentaccepts any origin — never run production withNODE_ENV=development. - No-origin requests are allowed. curl, mobile apps, and server-to-server calls (no
Originheader) always pass CORS — they are gated only by auth. totpRateLimitfails 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
sidskip the session-revoke check; they age out within the 15-minute access-token lifetime and pick up asidon the next refresh. - Token rotation vs. revoke. A
RefreshTokenrevoked with areplacedByToken(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.
morganlogging is off. Request logging is commented out;authMiddlewaredoes its ownconsole.debugper 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 leakserror.messagewhenNODE_ENV=development. X-Platformheader (multi-tenant). CORS allowsX-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)financeAuditis not a middleware. Audit trails for finance are written at the service layer (GstAuditLog), not in the request pipeline.
Related docs¶
- Identity & Access — roles, JWT issuance, refresh-token rotation
- Security Architecture — threat model, secrets, hardening
- Vendor API Platform — API-key lifecycle and access summaries
- Sales CRM — sales-head permission model
- Finance — finance portal + GST audit logging
- Background Jobs — BullMQ queues and workers