Backend
How the NestJS backend is organized — module grouping, the global pipeline, and the conventions every module follows.
The backend (apps/backend) is a NestJS 11 application and the only service
that talks to the database. It is the single API surface for every client:
the web apps, the mobile app, EMAs, and third-party developers.
Bootstrap
The entry point is apps/backend/src/main.ts. The notable global configuration:
- Global prefix
/api, with URI-based versioning (/api/v1,/api/v2, defaultVERSION_NEUTRAL). - A global
ValidationPipewithtransform,whitelist, andforbidNonWhitelisted— unknown fields are rejected, and DTOs are type-coerced. - Security: Helmet, plus CORS allowing the configured origins and a custom
header set (
X-Org-Id,X-Org-Role,X-Experience-Id,X-Acting-User-Id,Idempotency-Key). The server trusts the proxy (Railway/Vercel) for accurate client IPs. - Observability: Sentry instrumentation is loaded before bootstrap; Pino is the logger.
- Docs: three OpenAPI documents are served under
/api/docs(Internal / Developer / EMA), each with both Swagger UI and a Scalar reference.
Module organization
Modules live under apps/backend/src/modules/, grouped into domain folders.
Infrastructure concerns live under src/infrastructure/, and shared building
blocks under src/common/.
src/
├── modules/ feature modules, grouped by domain
│ ├── core/ feed, messaging, tasks, timeline, organizations
│ ├── academic/ students, teachers, courses, grades, attendance…
│ ├── family/ relationships, consents, guardians
│ ├── identity/ auth, JWT strategy, RBAC guards, permissions
│ ├── people/ roles, profiles, onboarding
│ ├── platform/ ops · ecosystem (EMA/webhooks/apikeys) · org-admin
│ ├── super-admin/ feature flags, admin credentials
│ └── … (communication, notifications, finance, content, …)
├── infrastructure/ database, redis, queue, throttler, convex, logger…
└── common/ guards, interceptors, filters, decorators, dto, scopingThe AppModule wires together infrastructure modules (global) and the domain
modules. The breakdown of every domain — and the rule for what becomes a core
module vs. an EMA — is on the Domains overview.
The global pipeline
The runtime behavior that applies to every request is assembled in
app-runtime.module.ts as an ordered chain. Order matters: each stage assumes
the previous ones have run.
Guards (in order)
| # | Guard | Responsibility |
|---|---|---|
| 1 | CustomThrottlerGuard | IP-based rate limiting (public bucket). |
| 2 | JwtAuthGuard | Verifies the JWT, attaches request.user. Bypassed by @Public(). |
| 3 | UserRateLimitGuard | Per-user, identity-aware rate limiting. |
| 4 | OrgGuard | Requires org context for authenticated non-super-admins. |
| 5 | MemberStatusGuard | Blocks removed members (cached, 300s TTL). |
| 6 | RolesGuard | Legacy org-role check via @Roles(...). |
| 7 | PermissionGuard | Fine-grained resource:action checks via @RequirePermission / @RequireOrgRole. |
| 8 | RelationshipGuard | Row-level access via @DataClass (e.g. guardian ↔ child). |
| 9 | ConsentGuard | Guardian consent for sensitive child-data classes. |
| 10 | EmaProxyGuard | Validates EMA proxy init-data (no-op when absent). |
Interceptors (in order)
| # | Interceptor | Responsibility |
|---|---|---|
| 1 | LoggingInterceptor | Structured request/response logging. |
| 2 | OrgScopeInterceptor | Resolves and attaches orgId / orgRole to the request. |
| 3 | EmaAuditInterceptor | Audit logging for EMA-scoped requests. |
| 4 | ChildDataAuditInterceptor | Fire-and-forget audit of child-data access. |
| 5 | RateLimitHeadersInterceptor | Adds X-RateLimit-* response headers. |
| 6 | TransformInterceptor | Wraps the handler result in the response envelope. |
Exception filters
FamilyExceptionFilter runs first (domain-specific family errors), then
AllExceptionsFilter catches everything else, shapes it into the error
envelope, and reports 5xx to Sentry.
See the full walk-through on the Request lifecycle page.
Conventions every module follows
- Tenant isolation in the data layer. Repositories extend
OrgScopedRepository, which injectsorgId(and soft-delete) filtering automatically. Rawdb.select()on tenant tables is forbidden (ADR-006). - Prefixed IDs. Entity IDs are generated with
generateId("entity"), producing readable values liketask_h4n2k9x...(ADR-007). - Side effects via the transactional outbox. Email, webhooks, and notifications are dispatched through an outbox + event handlers, not inline — this is what keeps delivery durable and free of duplicates (ADR-010).
- CQRS for domain events. A CQRS event bus (
src/common/cqrs/) handles async domain events and projections. - DTOs with
class-validator. Every input is a DTO class; custom validators like@IsPrefixedId()enforce ID shapes.