Drift control
The Permissions Matrix is the only piece of this site that cannot drift from the live API. Every other page is hand-written, so the matrix is the canonical gate.
How the generator works
Section titled “How the generator works”apps/docs/scripts/generate-permissions-matrix.ts runs at every build and at every dev start:
- It uses
ts-morphto load everyapps/api/src/**/*.controller.tsas a TypeScript AST. - For each
@Controller("<prefix>")class, it walks the methods. - For each method that carries an HTTP decorator (
@Get,@Post,@Patch,@Put,@Delete), it extracts:- The route path (controller prefix + method path, with the API’s global
/v1prefix prepended —healthis exempt, matchingapps/api/src/main.ts). - The literal arguments to
@Roles(...). - Whether the method or its class is
@Public(). - The relative source file path and line number.
- The route path (controller prefix + method path, with the API’s global
- It writes the result, sorted by
module→path→method, toapps/docs/src/generated/permissions-matrix.json. That file is gitignored: it exists only at build time. - It exits non-zero if any method has neither
@Roles(...)nor@Public()and isn’t on a*Public*Controllerclass.
What triggers a build failure
Section titled “What triggers a build failure”“Some controller methods declare no access posture.”
This is by design. Every new endpoint must opt into a posture, because “I forgot a decorator” must not become “anyone can call this endpoint.”
When the build fails with this error:
- Open the file/line printed in the error.
- Decide: should this endpoint require a role, or is it intentionally anonymous?
- Either:
- Role-gated — add
@Roles("member"),@Roles("teros-ops", "teros-ops-admin"), etc. - Public — add
@Public()to the method (or, if the whole controller is public, the class). Public routes still need their own authorization mechanism — e.g., a tokenized URL.
- Role-gated — add
- Re-run
pnpm --filter docs build; the matrix regenerates with the new posture.
What the matrix does not check
Section titled “What the matrix does not check”- It does not verify that
RolesGuardis actually registered globally. (It is, viaAuthModule; that’s a one-line audit.) - It does not verify field-level access. Hand-maintained tables in Database & Prisma cover the most permission-sensitive models for now.
- It does not check that
OrganizationScopeGuardis applied where it should be. That stays a code review concern.
Editing field-level permissions
Section titled “Editing field-level permissions”When a service changes its field filtering — e.g., a new field on CandidateProfile is added and only teros-ops* should be able to write it — update the relevant table in Database & Prisma. Drift between the table and the service stays hand-managed for now. A follow-up issue will explore automating this.
What to do if the matrix is wrong
Section titled “What to do if the matrix is wrong”The matrix can only be wrong in three ways:
- A decorator argument the extractor doesn’t understand (e.g., someone passes a spread of a non-literal
const). Fix the extractor inapps/docs/scripts/permissions-matrix-core.tsor refactor the decorator to use literal strings — the latter is the project’s preference. - A new HTTP decorator name that isn’t in the extractor’s switch. Add it to
HTTP_METHOD_DECORATORS. - A new “public” pattern the extractor doesn’t recognize. Prefer
@Public(); the naming-convention fallback exists only for the historical*Public*Controllerclasses.
The script lives next to the docs site, not inside apps/api. It is pure source analysis — no network, no database, no NestJS runtime — so it can run anywhere TypeScript can.