API (apps/api)
The API is a single NestJS application at apps/api. It is the only place authorization decisions are made; the frontends never enforce role checks themselves.
Module layout
Section titled “Module layout”Each apps/api/src/<module>/ directory follows the same shape:
applications/├── applications.controller.ts ← thin: routing, DTO, response shaping├── applications.service.ts ← business logic, talks to Prisma├── applications.module.ts ← Nest module wiring├── applications.integration.spec.ts ← Testcontainers-backed integration test└── dto/ ← class-validator DTOsModules currently in the tree include applications, audit, candidate-profiles, health, job-posting-requests, job-postings, ops-dashboard, organizations, and skills. There are also a few cross-cutting folders:
auth/—WorkOSGuard,RolesGuard, role helpers (hasRole,isOps,canAccessOrganization), the@Roles(...)and@OrganizationScope(...)decorators.common/decorators/—@Public(), field-access helpers.common/filters/— global exception filters (e.g.PrismaAvailabilityExceptionFilter).prisma/— thePrismaServicewrapper around@repo/database.storage/— S3 / file storage abstraction.
The guard chain
Section titled “The guard chain”Every protected route runs through both guards, in this order:
WorkOSGuard(apps/api/src/auth/workos.guard.ts) — verifies the JWT, fetches the WorkOS user/org context, normalizes the claims, and attachesrequest.auth = { sub, orgId, roles, ... }. Skips verification when@Public()is present.RolesGuard(apps/api/src/auth/roles.guard.ts) — reads theauth:rolesmetadata set by the@Roles(...)decorator and rejects requests whoseauth.rolesdoesn’t intersect with the declared set.
A route is considered “public” only when it carries @Public() (or its class does). Forgetting to declare either @Roles(...) or @Public() is not silently treated as “public” — the Permissions Matrix generator refuses to build the docs in that case, which keeps the API surface honest at PR time.
Organization scoping
Section titled “Organization scoping”Multi-tenant data is scoped through:
@OrganizationScope(...)+OrganizationScopeGuard— for routes that operate on a resource belonging to a specific organization.canAccessOrganization(auth, resourceOrgId, options)— the canonical helper inside services, allowingteros-opscross-org access while restrictingcompany-*roles to their ownorgId.@RequireOwnership(...)+OwnershipGuard— for resources owned by a single user (e.g. a candidate’s own profile).
DTOs and validation
Section titled “DTOs and validation”- Every request payload has a DTO under
<module>/dto/. - DTOs use
class-validatordecorators (@IsString,@IsOptional,@IsEnum,@ValidateNested, …). - A global
ValidationPipeinmain.tsenableswhitelist,transform, andforbidNonWhitelisted— extra fields are rejected, types are coerced, and unknown shapes fail fast.
Errors
Section titled “Errors”- Throw
HttpExceptionsubclasses (BadRequestException,NotFoundException,ForbiddenException, …) — never raw Postgres / Prisma errors. - The global
PrismaAvailabilityExceptionFilterconverts known Prisma availability errors into clean HTTP responses.
OpenAPI
Section titled “OpenAPI”The codebase preserves the ability to emit an OpenAPI YAML spec from the controllers and DTOs. Treat that capability as a contract: every new endpoint and DTO must remain compatible with @nestjs/swagger-style introspection, even when the spec is not produced on every build.
Where to look next
Section titled “Where to look next”- Permissions Matrix — every endpoint and its required roles.
- Drift control — how the matrix is generated and what to do when the build fails.
- Database & Prisma — the data model the services manipulate.