EmofyEmofy Developers
Architecture

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, default VERSION_NEUTRAL).
  • A global ValidationPipe with transform, whitelist, and forbidNonWhitelisted — 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, scoping

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

#GuardResponsibility
1CustomThrottlerGuardIP-based rate limiting (public bucket).
2JwtAuthGuardVerifies the JWT, attaches request.user. Bypassed by @Public().
3UserRateLimitGuardPer-user, identity-aware rate limiting.
4OrgGuardRequires org context for authenticated non-super-admins.
5MemberStatusGuardBlocks removed members (cached, 300s TTL).
6RolesGuardLegacy org-role check via @Roles(...).
7PermissionGuardFine-grained resource:action checks via @RequirePermission / @RequireOrgRole.
8RelationshipGuardRow-level access via @DataClass (e.g. guardian ↔ child).
9ConsentGuardGuardian consent for sensitive child-data classes.
10EmaProxyGuardValidates EMA proxy init-data (no-op when absent).

Interceptors (in order)

#InterceptorResponsibility
1LoggingInterceptorStructured request/response logging.
2OrgScopeInterceptorResolves and attaches orgId / orgRole to the request.
3EmaAuditInterceptorAudit logging for EMA-scoped requests.
4ChildDataAuditInterceptorFire-and-forget audit of child-data access.
5RateLimitHeadersInterceptorAdds X-RateLimit-* response headers.
6TransformInterceptorWraps 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 injects orgId (and soft-delete) filtering automatically. Raw db.select() on tenant tables is forbidden (ADR-006).
  • Prefixed IDs. Entity IDs are generated with generateId("entity"), producing readable values like task_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.

Next

On this page