Engineering Conventions & Code Style¶
This document is the canonical reference for how code is structured and written in the
mr-mentor-backend repository. It captures the real, observed conventions of the codebase —
the layered entity → service → controller → route flow, the class-based dependency-injected
Routes aggregator, the DataSource/Redis/Queue singletons, the TypeORM repository pattern,
the try/catch error-handling style, queue offloading for slow work, and the implications of
tsconfig running with strict: false. Use it to keep new code consistent with what already
exists, and to understand where a given piece of code belongs.
Status: documented from source on this branch.
Overview¶
mr-mentor-backend is a single Express + TypeScript monolith that hosts many MAS products
(Mr. Mentor mentorship + meetings + tokens, Mr. Hire recruitment/ATS, MAS LMS, Sales CRM,
Finance/GST, an AI platform, workflow automation, a vendor API, and more). Despite the breadth,
every domain follows the same five-layer skeleton, so a developer who learns one feature can
navigate any other.
Who relies on these conventions
| Persona | Why the conventions matter |
|---|---|
| Backend engineers adding a feature | The entity → service → controller → route → mount recipe is the contract for "where does my code go". |
Reviewers (/review, /code-review) |
Knowing controllers are thin and services hold logic makes "logic in the controller" an instant review flag. |
| New joiners | The DI-of-DataSource pattern and @/ alias are non-obvious without this doc. |
| AI assistants | strict: false + no-explicit-any: warn means the compiler will not catch many mistakes — conventions are the guardrail. |
The codebase began as an "express-typescript-template" (the package.json name field still
says so) and grew organically; conventions are therefore enforced by consistency and review,
not by strict tooling.
Key concepts & entities¶
This is a guideline doc, so the "entities" here are the structural building blocks rather than business tables.
| Term | What it is | Representative file |
|---|---|---|
| Entity | A TypeORM @Entity() class = one DB table. |
src/entities/Tokens.ts, src/entities/User.ts |
| Service | Holds all business logic; owns repositories; calls other services. | src/services/MentorService.ts |
| Controller | Thin HTTP adapter: parse req, call service, shape res.json. |
src/controllers/mentors.controller.ts |
| Route class | Class exposing public router, wires paths + middleware to controller methods. |
src/routes/mentor.routes.ts |
Routes aggregator |
Top-level class that instantiates every route class with the shared DataSource and mounts each under a prefix. |
src/routes/index.ts |
App |
Builds the Express app: middleware, routes, error handlers, lifecycle. | src/app.ts |
DatabaseService |
Singleton wrapping the TypeORM DataSource. |
src/config/database.ts |
RedisService |
Singleton ioredis client. | src/config/redis.ts |
QueueService |
Singleton owning all BullMQ Queue producers. |
src/services/QueueService.ts |
| Middleware | authMiddleware, adminMiddleware, expertMiddleware, etc. — pure functions. |
src/middleware/ |
@/ alias |
Import alias for src/ (e.g. import { X } from "@/entities/X"). |
tsconfig.json, package.json _moduleAliases |
Architecture¶
The request path is strictly layered. Each layer only talks to the one directly below it.
flowchart TD
Client["HTTP / Socket.IO client"]
subgraph APP["Express App (src/app.ts)"]
MW["Middleware (cors, helmet, json, authMiddleware, role guards)"]
AGG["Routes aggregator (src/routes/index.ts)"]
RC["Route class e.g. MentorRoutes (src/routes/*.routes.ts)"]
CTRL["Controller e.g. MentorsController (src/controllers/*.ts)"]
SVC["Service e.g. MentorService (src/services/*.ts)"]
end
subgraph SINGLETONS["Process singletons"]
DB["DatabaseService.dataSource (TypeORM)"]
REDIS["RedisService.client (ioredis)"]
Q["QueueService (BullMQ producers)"]
end
ENT["Entities / Repositories (src/entities/*.ts)"]
PG[("PostgreSQL")]
WORKERS["BullMQ workers (src/workers/*.ts)"]
EXT["External: S3, Razorpay, Gmail SMTP, Google, Exotel, LLM gateway"]
Client --> MW --> AGG --> RC --> CTRL --> SVC
SVC --> ENT --> DB --> PG
SVC --> REDIS
SVC --> Q --> REDIS
Q -.enqueues.-> WORKERS
WORKERS --> ENT
WORKERS --> EXT
SVC --> EXT
The golden rule: controllers are HTTP-only; services are framework-agnostic business logic.
A controller never touches a TypeORM repository directly, and a service never touches req/res.
Data model¶
There are no business tables owned by this doc, but the structural composition pattern — how a
Routes aggregator owns route classes, which own controllers, which own services, which own
repositories — is itself a "model" worth drawing.
erDiagram
ROUTES_AGG ||--o{ ROUTE_CLASS : "instantiates"
ROUTE_CLASS ||--|| CONTROLLER : "owns one"
CONTROLLER ||--o{ SERVICE : "calls"
SERVICE ||--o{ REPOSITORY : "gets from DataSource"
REPOSITORY ||--|| ENTITY : "maps"
SERVICE ||--o{ SERVICE : "composes"
SERVICE ||--o{ QUEUE : "enqueues to"
ROUTES_AGG {
DataSource dataSource
Router router
}
ROUTE_CLASS {
Router router PK
string mountPrefix
}
CONTROLLER {
arrow_fn handlers
}
SERVICE {
Repository repos
Service deps
}
ENTITY {
decorator Entity
uuid id PK
}
API surface¶
This doc does not own endpoints. The conventions for the API surface, derived from
src/routes/index.ts and the route classes, are:
| Aspect | Convention | Evidence |
|---|---|---|
| Global prefix | Almost everything mounts under /api. |
this.router.use('/api', ...) in src/routes/index.ts |
| Admin sub-prefix | Admin features mount under /api/admin. |
this.router.use('/api/admin', this.adminRoutes.router) |
| Feature sub-prefix | Some classes get a dedicated prefix (e.g. /api/finance, /api/sales, /api/v1/vendor, /api/candidate-quiz). |
src/routes/index.ts lines mounting financeRoutes, vendorRoutes, etc. |
| Path declaration | Inside a route class, paths are relative and declared in initializeRoutes(). |
this.router.get('/mentors', ...) in src/routes/mentor.routes.ts |
| Auth | Applied per-route as middleware args, not globally. | this.router.get('/...', authMiddleware, expertMiddleware, ctrl.method) |
| Public vs protected | Same router mixes public and protected routes; protection is opt-in per line. | mentor.routes.ts (e.g. /mentors public, /mentors/multiplier guarded) |
| Bull Board UI | Mounted at root, not /api. |
this.router.use('/', this.bullBoardRoutes.router) |
When adding an endpoint, derive the full path as
<mountPrefix in index.ts>+<relative path in the route class>. Do not hard-code a full/api/...path inside a route class.
User journeys¶
These journeys describe the developer workflows and the runtime control flow that the conventions produce — they are the headline of this guideline.
Journey 1 — A request flows through the layers (happy path)¶
A GET /api/mentors/:mentorId/slots/available request, end to end.
sequenceDiagram
participant FE as Frontend
participant MW as Middleware chain
participant RC as MentorRoutes
participant CTRL as MentorsController
participant SVC as MentorService
participant REPO as TypeORM repository
participant PG as PostgreSQL
FE->>MW: GET /api/mentors/ID/slots/available
MW->>RC: cors then helmet then json pass through
RC->>CTRL: route matched then call getMentorAvailableSlots
CTRL->>SVC: parse params then call service method
SVC->>REPO: slotsRepository.find with where clause
REPO->>PG: SELECT query
PG-->>REPO: rows
REPO-->>SVC: entity objects
SVC-->>CTRL: plain data
CTRL-->>FE: res.status 200 json with status OK and data
The controller method is an arrow-function class property (so this is bound) wrapped in a
try/catch; the service does the querying. See "annotated snippets" below.
Journey 2 — A protected, role-gated request¶
POST /api/mentors/slots/available requires a logged-in EXPERT.
sequenceDiagram
participant FE as Frontend
participant AUTH as authMiddleware
participant EXP as expertMiddleware
participant DB as DatabaseService
participant CTRL as MentorsController
participant SVC as MentorService
FE->>AUTH: POST with Bearer token or cookie
AUTH->>AUTH: read token from header or cookie
AUTH->>DB: getRepository User then findOne by id
DB-->>AUTH: user row with isActive and role
alt token missing or user blocked or session revoked
AUTH-->>FE: 401 or 403 json error
else valid
AUTH->>EXP: attach req.user then next
EXP->>EXP: check req.user.role is EXPERT
alt wrong role
EXP-->>FE: 403 Access denied
else allowed
EXP->>CTRL: next then call markSlotsAvailable
CTRL->>SVC: delegate to service
SVC-->>CTRL: result
CTRL-->>FE: 200 json
end
end
Key convention: auth state lives on req.user (typed via the AuthenticatedRequest
interface in src/middleware/auth.middleware.ts), and DB re-verification happens inside
authMiddleware itself (account-blocked, password-changed, session-revoked checks).
Journey 3 — Offloading slow work to a queue (async)¶
Services never block an HTTP response on email/IO-heavy work. They enqueue and return.
sequenceDiagram
participant CTRL as Controller
participant SVC as Service
participant Q as QueueService
participant REDIS as Redis
participant WORKER as BullMQ worker
participant EXT as Gmail SMTP or S3 or external
CTRL->>SVC: call business method
SVC->>SVC: do the DB write synchronously
SVC->>Q: add job to a queue such as emailQueue
Q->>REDIS: persist job
SVC-->>CTRL: return immediately
CTRL-->>CTRL: respond to client without waiting
Note over WORKER: separate process pulls the job
WORKER->>REDIS: fetch next job
WORKER->>EXT: perform slow side effect
alt success
WORKER->>REDIS: mark complete and keep last 50
else failure
WORKER->>REDIS: mark failed for retry or inspection
end
QueueService is a singleton holding 20+ named Queue producers (emailQueue,
notificationQueue, resumeAnalysisQueue, mrlearnSyncQueue, workflowQueue, …) all pointed
at the same Redis connection with removeOnComplete/removeOnFail retention. See
src/services/QueueService.ts.
Journey 4 — Adding a new feature (developer workflow)¶
The canonical "where does my code go" recipe, expressed as a flow.
flowchart TD
START["New feature request"]
Q1{"Needs a new DB table?"}
E["Create entity in src/entities and register it in src/config/database.ts entities array"]
Q2{"Has business logic / DB access / external calls?"}
S["Put logic in a Service (src/services). Inject DataSource then getRepository in constructor"]
C["Create thin Controller (src/controllers) with arrow-fn handlers wrapping try/catch"]
R["Create Route class (src/routes/*.routes.ts) exposing public router and initializeRoutes"]
M["Register + instantiate the route class in src/routes/index.ts and mount under a prefix"]
Q3{"Slow side effect (email, AI, big IO)?"}
W["Enqueue via QueueService and handle in a worker (src/workers)"]
DONE["Done"]
START --> Q1
Q1 -- yes --> E --> Q2
Q1 -- no --> Q2
Q2 -- yes --> S --> C
Q2 -- no --> C
C --> R --> M --> Q3
Q3 -- yes --> W --> DONE
Q3 -- no --> DONE
This mirrors the "Adding a new feature" steps in the repo's own CLAUDE.md.
Background jobs & async¶
| Convention | Detail | File |
|---|---|---|
| One singleton producer | All BullMQ Queue objects live on QueueService (singleton). Producers are created in its private constructor. |
src/services/QueueService.ts |
| Workers run in-process | Workers are started during the boot sequence (see src/index.ts), one file per queue. |
src/workers/*.worker.ts |
| Retention | Jobs use removeOnComplete / removeOnFail (50 or 100) to bound Redis growth. |
QueueService constructor |
| Scheduled jobs | Cleanup (~24h) and KPI (~15min) are scheduled at startup; sync queues (mrlearn/mrtest/aarya) and reminders run on their own cadence. | src/index.ts, src/workers/ |
| Socket.IO | Real-time meeting/recording/presence logic lives in src/socket.ts (large, separate from the HTTP layers). |
src/socket.ts |
| Never block responses | If an action triggers email/AI/heavy IO, the service does the DB write then enqueues; it does not await the side effect before responding. |
pattern across services |
Workers present (non-exhaustive): email, database, cleanup, kpi, resumeAnalysis,
salaryBenchmark, warning, leadAssignment, daily-cards, course-plan, missOzone,
mrlearnSync, mrlearnReminder, mrlearnNewStudentSync, mrtestSync, badgeEvaluation,
studentRiskComputation, assignmentReminder, aaryaSync, workflow, whatsapp.
External integrations¶
The conventions for talking to third parties:
| Convention | Detail |
|---|---|
| Wrap in a service | Each integration has a dedicated service (EmailService, ExotelService, LlmGatewayService, GoogleCalendarService, LeegalitySandboxService, S3 uploader services). Controllers never call SDKs directly. |
Config via process.env |
Read env vars with a sensible fallback: process.env.REDIS_HOST || 'localhost'. No config library; dotenv.config() is called at the top of src/app.ts. |
| Feature flags / kill switches | src/utils/featureFlags.ts exposes FeatureFlags.* helpers that default ON and turn OFF only when the env var equals the string "false" or "0". Restart required after a change. |
| Graceful disable | Optional integrations (e.g. Exotel) self-disable when their env vars are unset rather than crashing — feature falls back. |
| Secrets | Never hard-coded; loaded from env (deployed via AWS Secrets Manager). See repo CLAUDE.md. |
Status lifecycles¶
The relevant "lifecycle" for this guideline is the application boot sequence in
src/app.ts / src/index.ts, which is itself a state machine the conventions depend on
(singletons must be initialized before routes serve traffic).
stateDiagram-v2
[*] --> Constructing
Constructing --> MiddlewareReady : App constructor wires cors helmet json
MiddlewareReady --> RoutesMounted : new Routes dataSource then mount under api
RoutesMounted --> DbInitializing : listen called
DbInitializing --> SchemasEnsured : ensure named schemas mrlearn mrtest mas_crm superadmin
SchemasEnsured --> DbSynced : dataSource initialize with synchronize true
DbSynced --> Seeded : seedColleges then seedUsers
Seeded --> RedisReady : redis initialize
RedisReady --> WorkersStarted : BullMQ workers start
WorkersStarted --> Listening : http server listen on PORT
Listening --> [*]
DbInitializing --> Failed : error then process exit 1
Failed --> [*]
DatabaseService, RedisService, and QueueService are all the singleton pattern:
private constructor() + static getInstance(). Never new them directly; always
getInstance().
Annotated code snippets (good / bad)¶
Route class — the construction pattern¶
Every route file exports a class with a public router, builds its controller in the
constructor, and declares relative paths in a private initializeRoutes(). From
src/routes/mentor.routes.ts:
export class MentorRoutes {
public router: Router;
private readonly mentorController: MentorsController;
constructor(dataSource: DataSource) { // DataSource injected from Routes aggregator
this.router = Router();
this.mentorController = new MentorsController(dataSource);
this.initializeRoutes();
}
private initializeRoutes(): void {
this.router.get('/mentors', this.mentorController.getMentors); // public
this.router.post('/mentors/slots/available',
authMiddleware, expertMiddleware, // per-route guards
this.mentorController.markSlotsAvailable);
}
}
The class is then declared, instantiated with the shared dataSource, and mounted in
src/routes/index.ts:
this.mentorsRoutes = new MentorRoutes(dataSource); // in the Routes constructor
// ...
this.router.use('/api', this.mentorsRoutes.router); // in initializeRoutes()
Controller — thin, arrow-fn, try/catch (GOOD)¶
From src/controllers/mentors.controller.ts. Note: handlers are arrow-function class
properties (preserves this), they parse the request, delegate to the service, and shape the
response. The common success shape is { status: 'OK', data }; the common error shape is
{ success: false, message, error }.
export class MentorsController {
private readonly mentorsService: MentorService;
constructor(dataSource: DataSource) {
this.mentorsService = new MentorService(dataSource); // DI down to the service
}
public getMentors = async (req: Request, res: Response): Promise<void> => {
try {
const mentorsData = await this.mentorsService.getMentors(); // logic lives in service
res.status(200).json({ status: 'OK', data: mentorsData });
} catch (error) {
res.status(500).json({
success: false,
message: 'Internal server error',
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};
}
Note
error instanceof Error ? error.message : 'Unknown error'— this is the repo's standard way to safely narrow a caughtunknown/anyerror (astrict: falsecodebase convention).
Controller doing too much (BAD — do not do this)¶
// BAD: business logic + raw repository access inside the controller.
public createSlot = async (req: Request, res: Response) => {
const repo = DatabaseService.getInstance().dataSource.getRepository(Slots); // ❌ DB in controller
const slot = repo.create({ ...req.body, status: 'available' }); // ❌ logic in controller
if (await repo.findOne({ where: { startTime: req.body.startTime } })) { // ❌ rules in controller
return res.status(409).json({ message: 'conflict' });
}
await repo.save(slot);
await sendEmail(...); // ❌ direct side effect, blocks response
res.json(slot);
};
The fix: move repository access, the conflict rule, and the email into a SlotService method;
enqueue the email via QueueService; keep the controller to parse → call → respond.
Service — owns repositories via the injected DataSource (GOOD)¶
From src/services/MentorService.ts. The service constructor resolves every repository it needs
and composes other services. Repositories are obtained with dataSource.getRepository(Entity) —
never via a global import.
export class MentorService {
public userRepository: Repository<User>;
public slotsRepository: Repository<Slots>;
public tokenService: TokenService;
public queueService: QueueService;
constructor(dataSource: DataSource) {
this.userRepository = dataSource.getRepository(User);
this.slotsRepository = dataSource.getRepository(Slots);
this.tokenService = new TokenService(dataSource); // compose sibling services
this.queueService = QueueService.getInstance(); // singleton, not new
}
public async getMentors(): Promise<MentorResponse> {
const mentors = await this.userRepository.find({
relations: ['mentorProfile'],
where: { role: UserRole.EXPERT },
});
for (const mentor of mentors) {
delete (mentor as any).password; // strip secrets before returning (strict:false `as any`)
}
return { status: 'OK', mentors };
}
}
Entity — decorator style (GOOD)¶
From src/entities/Tokens.ts. Conventions: @Entity('table_name') with an explicit snake/camel
table name, @PrimaryGeneratedColumn('uuid') ids, explicit { type, nullable, default } on
columns, relations with explicit @JoinColumn, and a small amount of domain behaviour as
methods is acceptable.
@Entity('tokens')
export class Token {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'float', nullable: true, default: 0 })
token: number | null;
@OneToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;
@Column({ type: 'uuid' })
userId: string;
getEarningValue(): number { // small domain helper on the entity is OK
if (!this.token) return 0;
return this.token / TOKEN_VALUE;
}
}
Every new entity MUST be added to the
entities: [...]array insrc/config/database.ts, otherwise TypeORM will not create/sync its table (auto-syncsynchronize: trueonly manages registered entities).
Naming conventions¶
Observed, consistent naming (mixed historically but trending toward the right column):
| Thing | Convention | Examples |
|---|---|---|
| Entity files | PascalCase.ts; newer ones add .entity.ts |
User.ts, Tokens.ts, ResumeAnalysis.entity.ts |
| Entity classes | PascalCase singular |
User, MentorEarnings, JobApplication |
| Service files & classes | PascalCaseService.ts |
MentorService.ts, EmailService.ts, FinanceReportsService.ts |
| Controller files | mixed: name.controller.ts (newer) or PascalCaseController.ts |
mentors.controller.ts, JobPostController.ts |
| Controller classes | PascalCaseController |
MentorsController, FinanceController |
| Route files | name.routes.ts |
mentor.routes.ts, finance.routes.ts |
| Route classes | PascalCaseRoutes (or a createXRoutes(dataSource) factory function for some) |
MentorRoutes, createSalesRoutes(...) |
| Route paths | kebab-case | /mentors/slots/available, /candidate-quiz |
| DB column names | mostly camelCase quoted identifiers ("createdAt"), some snake_case in CRM tables |
userId, createdAt, session_year |
| Imports | @/... alias OR relative ../... (both appear; alias preferred for cross-domain) |
import { DiscountRequest } from '@/entities/DiscountRequest' |
Inconsistency exists (e.g.
mentors.controller.tsvsJobPostController.ts). Prefer thename.controller.ts/name.routes.ts/PascalCaseService.ts/Name.entity.tsforms for new code, but match the surrounding domain's style when extending an existing feature.
TypeORM patterns¶
- One DataSource, injected everywhere.
DatabaseService.getInstance().dataSourceis the singleDataSource. TheRoutesaggregator receives it once and threads it into every route class → controller → service. Do not create newDataSources. - Repositories obtained in service constructors via
dataSource.getRepository(Entity); stored aspublic/readonlyfields. Query with.find,.findOne,.save,.create, query builders, and operators (In,MoreThan,IsNull) imported fromtypeorm. synchronize: trueis ON (src/config/database.ts) — entity changes auto-apply to the DB in dev. There are no hand-written migrations in the normal flow (amigrations/dir exists and is loaded if present, but the day-to-day model is auto-sync).- Non-default schemas (
mrlearn,mrtest,mas_crm,superadmin) are pre-created with raw SQL beforedataSource.initialize()becausesynchronizeonly creates tables, not schemas. - One-shot data migrations for tricky changes are done with raw
QueryRunnerSQL guarded to be idempotent (seemigrateLegacyRawLeadsToMasCrmandmigrateSuperadminRoleBeforeSyncinsrc/config/database.ts).
Error handling & logging¶
- Per-handler try/catch is the norm. There is a global error handler in
src/app.ts(returns{ success: false, message: 'Internal Server Error' }, leakingerror.messageonly whenNODE_ENV === 'development'), but controllers generally catch locally and return a structured JSON error rather than relying on it. error instanceof Error ? error.message : 'Unknown error'is the standard narrowing idiom for caught errors (necessary becausecatchbindsunknown/any).- Status codes:
200(and a{ status: 'OK', data }body) for success,400for bad input,401missing/invalid auth,403wrong role / blocked account,404not found (also the catch-all route),409conflicts,500for unhandled. - Logging is
console.*— no logging library. Useconsole.infofor lifecycle,console.warnfor recoverable issues,console.errorfor failures,console.debugfor verbose tracing. Emoji prefixes (✅,❌,⚠️,📡) are used widely as visual log markers.morganis a dependency but the request logger line is commented out insrc/app.ts.
ESLint / tsconfig implications¶
.eslintrc.js:
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn', // any is allowed, just warned
'prefer-const': 'error',
'no-var': 'error',
}
tsconfig.json runs with strict: false and strictPropertyInitialization: false. Practical
consequences you must keep in mind:
null/undefinedare not checked. The compiler will not flag a missing null guard — add them by hand. Many entity fields areT | null.anyflows freely (only a warning), so type errors that strict mode would catch slip through. The(mentor as any).passworddeletes and the safe-error-narrowing idiom are direct consequences.- Class fields need no initializer (
strictPropertyInitialization: false), which is why entities and services can declarepublic repo: Repository<X>;without!or assignment. - Decorators on (
experimentalDecorators+emitDecoratorMetadata) — required for TypeORM@Entity/@Columnandreflect-metadata(imported first insrc/app.tsandsrc/index.ts). - Build is via esbuild (
npm run build), nottsc, so type errors do not fail the build — onlynpm run lintand runtime catch problems. Runnpm run lintbefore pushing.
Edge cases, limits & gotchas¶
- Register every new entity in the
entities: [...]array insrc/config/database.ts, or its table will never be created (auto-sync silently ignores unregistered classes). - Singletons only via
getInstance()—DatabaseService,RedisService,QueueService. Callingnewwill create a second instance with its own connections/pool. @/alias works because of two registrations:tsconfig.jsonpaths(forts-nodeviatsconfig-paths/register) andpackage.json_moduleAliases(for runtime viamodule-alias/register, bundled by esbuild). Both must stay in sync.- Auth is opt-in per route. A route with no
authMiddlewareis public. Several mentor/bot/webhook routes are intentionally public — do not assume a router is protected. adminMiddlewareis exact-matchADMIN. ADMIN is the universal-access role; it inherits everything. Note the formerSUPERADMINrole was removed and migrated toadmin— see the pre-sync migration insrc/config/database.tsand the role enum insrc/types/UserTypes.ts(USER,ADMIN,EXPERT,SALES,SALES_HEAD, …).- Multi-platform via
x-platformheader. Auth and some flows branch onreq.headers['x-platform'](mr-mentorvsmy-analytics-schoolvsmr-hire). It is allowed as a CORS header (X-Platforminsrc/app.ts) and drives cross-platform lead creation insrc/controllers/auth.controller.ts. New cross-product behaviour should read this header, not guess from the origin. - Feature flags default ON.
FeatureFlags.*(src/utils/featureFlags.ts) only disable on the literal strings"false"/"0"; a process restart is required after changing the env var. - CORS in development is wide open (
NODE_ENV === 'development'allows any origin insrc/app.ts). Production relies onCORS_ALLOWED_ORIGINS(a JSON array string) plus the hard-coded default origins. - Body limit is 20mb (
express.json({ limit: '20mb' })) — larger uploads must go through the S3 presigned-URL path, not the JSON body. - Tests run on Bun (
bun test) while runtime is Node 20 — keep test code Bun-compatible.
Related docs¶
- System Architecture Overview
- Data Model Reference
- Background Jobs & Queues
- Authentication & Authorization
- DevOps & Deployment
- Repo root developer guide:
mr-mentor-backend/CLAUDE.md
Sibling-doc links are relative to
docs/guidelines/; adjust if the surrounding tree differs.