Skip to content

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.

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 DTOs

Modules 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/ — the PrismaService wrapper around @repo/database.
  • storage/ — S3 / file storage abstraction.

Every protected route runs through both guards, in this order:

  1. WorkOSGuard (apps/api/src/auth/workos.guard.ts) — verifies the JWT, fetches the WorkOS user/org context, normalizes the claims, and attaches request.auth = { sub, orgId, roles, ... }. Skips verification when @Public() is present.
  2. RolesGuard (apps/api/src/auth/roles.guard.ts) — reads the auth:roles metadata set by the @Roles(...) decorator and rejects requests whose auth.roles doesn’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.

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, allowing teros-ops cross-org access while restricting company-* roles to their own orgId.
  • @RequireOwnership(...) + OwnershipGuard — for resources owned by a single user (e.g. a candidate’s own profile).
  • Every request payload has a DTO under <module>/dto/.
  • DTOs use class-validator decorators (@IsString, @IsOptional, @IsEnum, @ValidateNested, …).
  • A global ValidationPipe in main.ts enables whitelist, transform, and forbidNonWhitelisted — extra fields are rejected, types are coerced, and unknown shapes fail fast.
  • Throw HttpException subclasses (BadRequestException, NotFoundException, ForbiddenException, …) — never raw Postgres / Prisma errors.
  • The global PrismaAvailabilityExceptionFilter converts known Prisma availability errors into clean HTTP responses.

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.