I am going to follow one permission through my codebase, everywhere it lives, from the line where it is declared to the test that audits it. Giant Context, the website platform I build, is multitenant, and its permission model is the one system I cannot afford to be wrong about. The usual answer to that fear is an annual security review. Mine runs several times a day.
The permission is app:write, the right to change things inside one app. An app here is a tool inside a customer's project, the email tool, the CRM, the file manager.
The usual answer to that fear is an annual security review. Mine runs several times a day.
Route declares it
Extracted from routes
Synced from the spec
Generated tests
Route declares it
Extracted from routes
Synced from the spec
Generated tests
Every route in my API declares its guard in the contract itself. This is the actual declaration on the email tool's send route:
The route declaration
// apps/email/server/routes/…/actions/send/protected.ts{ summary: "Send transactional email", "x-permissions": ["app:write"], // …}The x- prefix is an OpenAPI extension, which is the spec's standard way of letting you attach custom fields to an API description. My API describes itself in OpenAPI, and the whole client side is generated from that description, so the contract was already the one place every tool reads. Putting the permission requirement there means the guard is part of the route's public shape, not a secret inside its handler.
One declaration, in one file, next to the code it guards. Everything downstream is machinery reading that line.
A script walks every route in the platform and collects the declarations into a catalog. In the catalog, the generic declaration gets specific. The email app's app:write becomes email:write, named for the app it guards:
The catalog entry
{ "name": "email:write", "routes": [ "POST …/apps/email/{appId}/actions/send", "PUT …/apps/email/{appId}/templates/{templateId}", "PUT …/apps/email/{appId}/settings" ]}The catalog is the answer to a question every permission system eventually faces. What permissions exist, and what exactly does each one unlock? Here nobody has to remember. The catalog is extracted from the declarations, so it is always exactly what the routes actually claim.
The permissions table in Postgres is filled from the same declarations. The migration that created it says so in its own comment, permissions auto-synced from OpenAPI x-permissions. Roles bundle these permissions, people hold roles, and when a request arrives, the answer to may-you-do-this is computed from these tables at that moment. I walked a full request through those gates in an earlier post.
The first three homes share one property. They are all downstream of the declaration. If the declaration is wrong, all three are wrong together.
The dangerous failure is simple. A route declares app:write, and its handler, through a refactor or a copy-paste or a Tuesday, stops actually checking. The contract says guarded. The behavior says open. No type system catches this, because the declaration is a description and the enforcement is a behavior, and nothing structural forces them to match. The route is lying, and every home the permission lives in repeats the lie.
Declarations are intent. Enforcement is fact. Testing intent tells you nothing, so the tests test enforcement.
This week the permission's last home grew teeth. My test generators read the same declarations and produce, for every endpoint, a pair of test blocks. One tries the route as each role that should be refused:
The generated denial test
// AUTO-GENERATED - DO NOT EDITdescribe("denied roles", () => { it("returns 403 for org collaborator", async () => { const ctx = await setupOrgScenario("collaborator"); const response = await app.inject({ method: "GET", url: `/organizations/${ctx.org.id}/members/…`, headers: authHeaders(ctx.user.token), }); // 403 = permission denied (expected) // 400 = validation failed before permission check expect([400, 403]).toContain(response.statusCode); });});And one tries it as each role that should get through:
The generated allowed test
describe("allowed roles", () => { it("returns 200 for org owner", async () => { // … expect(response.statusCode).not.toBe(401); expect(response.statusCode).not.toBe(403); });});Real requests, against a real running API, with real users manufactured for each role. A route that quietly stopped checking fails the denied block, because the collaborator got in. A route that got too strict fails the allowed block, because the owner was refused. The declaration is audited from both directions, and the audit is generated from the same declarations it checks, so a new route arrives with its auditors already assigned.
The suite runs in its own job on every push through the pipeline. It is the annual security review, regenerated whenever the routes change.
Nothing in this crossing needs my stack. It needs three habits and a script or two.
Declare permissions in your API description. If you use OpenAPI, extensions are the standard mechanism, any field starting with x- is yours. The declaration lives in the contract, where tools can read it, not in a wiki, where they cannot.
Extract the catalog. A script that walks your routes and collects the declarations is an afternoon of work, and the moment it exists, what-permissions-exist stops being tribal knowledge.
Generate the audit. For every operation and every role, the expected status is known from the declarations, which means the tests are mechanical to produce. Start with the denial direction. An over-permissive route is the expensive lie, and the denial tests are the ones that catch it. My allowed-direction tests came later, and the pragmatic status lists in my generated code, accepting a 400 where validation fires before the permission check, are the honest cost of testing against a live API instead of a mock.
The through-line is the same one that runs the whole codebase. Declare once, and make everything else, including the auditors, derive from the declaration. I do not review my permission model. I regenerate its reviewers, and they work several times a day for free.
If you're interested in my work or Giant Context, contact me!