Architecture
The monorepo's three apps, the shared library, and the exact path a request takes through the server.
The Naalya backend is a NestJS monorepo: several apps in one repository, sharing one set of dependencies and one pile of common code. The split exists because not all work is request work — answering HTTP should be fast, while email, audit writes, and external syncs are slow and retryable, so they run in separate processes.
Three things to hold in your head:
server— the HTTP API that answers the frontend.workerandaudit— background processes, no HTTP, just jobs off a queue.libs/shared(imported as@app/shared) — code all three reuse.
Default project is server
Projects are declared in nest-cli.json; the default is server, so pnpm start:dev with no arguments runs the API. The worker and audit apps have their own scripts. If a command "does nothing," check which project it targeted.
Repo layout
apps/
server/ → the HTTP API
worker/ → background job processor
audit/ → audit-log microservice
libs/
shared/ → code shared across all apps (@app/shared)
database/
migrations/ → TypeORM migrations (root-level, app-agnostic)
emails/ → React Email templatesEach app is self-contained, with its own main.ts and root module. Apps never import each other — the only shared code comes through @app/shared.
What the apps talk to
Four backing services, and in development only two of them are on your machine — see Getting Started for the split.
| Service | Role | Reached through |
|---|---|---|
| Postgres | Every domain row. Hosted on Neon. | TypeORM, BaseRepository |
| RabbitMQ | Queues, and the agent-tool RPC seam. Hosted on CloudAMQP. | BullMQ, Nest microservice clients |
| Redis | BullMQ backing store, and Centrifugo's engine. | REDIS_URL |
| Centrifugo | Websocket broker for realtime. | RealtimeService |
There is also a type bus: the server publishes shared TypeScript types at /.well-known/chowbea.json, which the Education Hub generates from. Types that never appear on a REST body — websocket payloads, domain enums — cross there instead of through Swagger. See How Chowbea works.
The three apps
| App | Boot file | Listens on | Hosts |
|---|---|---|---|
| server | apps/server/src/main.ts | HTTP port 8000, prefix api/v1 | ~48 feature modules |
| worker | apps/worker/src/main.ts | naalya-worker RMQ queue | Email, PDF, KB-embedding and payment-charge processors — plus the Rover agent graph |
| audit | apps/audit/src/main.ts | naalya-audit RMQ queue | AuditLogProcessor |
server — Express 5 HTTP server. Its root module apps/server/src/app/app.module.ts wires the database, the Redis/BullMQ connection, and one feature module per domain. You add a feature module and register it here; you rarely edit app.module.ts directly. Every route sits under api/v1 (so a users route is /api/v1/users) — the lone exception is /queues*, excluded so the Bull Board dashboard stays mounted at /queues.
worker is more than a queue consumer. Alongside its BullMQ processors it hosts naalya-agent/ — the LangGraph graph behind Rover and the role subagents — which reaches school data by calling back into the server over RabbitMQ rather than touching the database itself. That seam is the whole security model: How the AI system works.
audit is the simple one: a NestJS app with no HTTP server consuming one queue. It runs in its own process for isolation, so writing audit records never slows an API request. How the queues work → Queues & Messaging.
libs/shared
@app/shared is exposed through one barrel file (libs/shared/src/index.ts). Import from the package name, never a deep path:
import { BaseRepository, UserType, generateSlug } from '@app/shared';| Group | What you'll find |
|---|---|
| Database | DatabaseEntity, BaseRepository, DatabaseModule, cursor-pagination helpers |
| Config | config, validateConfig, env types |
| API helpers | MessageResponseDto, ErrorResponseDto, Swagger error decorators |
| Enums | UserType, ApplicationStatus, ScopeLevel, DepartmentRole, … |
| Queue contracts | queue names, job-payload types, microservice tokens |
Permissions live on a subpath
The CASL Action / Resource enums and PermissionScopeModule come from @app/shared/permission, not the top-level barrel. An Action import that "can't be found" is usually pointed at @app/shared.
Path aliases
Defined in tsconfig.json (and mirrored in the Vitest configs). Use them before reaching for a relative ../ chain.
"paths": {
"@app/shared": ["libs/shared/src"],
"@app/shared/*": ["libs/shared/src/*"],
"@server/*": ["apps/server/src/*"],
"@worker/*": ["apps/worker/src/*"]
}Rule of thumb: @app/shared for anything reused across apps, @server/* for paths inside the server app.
Request lifecycle
A request to the server app passes through a fixed assembly line before reaching your handler. Order is set in main.ts (middleware, pipe, interceptor, filter) and auth.module.ts (guards).
HTTP request
▼ Express middleware (permissionScope → tenantContext → requestId → cookieParser)
▼ Global guards (JwtAuthGuard → UserTypeGuard → SystemRoleGuard → PermissionsGuard)
▼ ValidationPipe (validate + transform the DTO)
▼ Controller handler (your code)
▼ LoggingInterceptor (wraps the response)
▼ Exception filters (only if something threw)
▼ HTTP response| Stage | Job |
|---|---|
| Middleware | Plain Express, runs before Nest. tenantContextMiddleware resolves the active school into AsyncLocalStorage so BaseRepository auto-scopes by schoolId — see Multi-tenancy. |
| Guards | Four APP_GUARDs, ordered cheapest-check-first: logged in → user type → system role → CASL permission. Short-circuit on first failure. Details → Auth & Permissions. |
| ValidationPipe | Runs the controller DTO's class-validator rules; bad input becomes a clean 400, never a crash. |
| Handler | Your controller method. The only stage you usually write. |
| LoggingInterceptor | Wraps the response, logging it with the request ID. |
| Exception filters | AllExceptionsFilter formats throws into the standard error shape; SentryGlobalFilter reports them. Run only on failure. |
Guard order is load-bearing
Authentication (JwtAuthGuard) runs before authorization (PermissionsGuard) and the chain stops on the first false. Don't assume a later guard ran.
Where to go next
Module Anatomy
Open one feature module and see how a controller, service, and entity fit together.
Auth & Permissions
The full rules behind the four guards and the CASL permission model.
Queues & Messaging
How the worker and audit apps consume jobs off BullMQ over RabbitMQ.
Getting Started
Get the monorepo installed and the server running on your machine.