Add an API route

Adding a route handler under applications/main/src/app/api/**, and choosing the right permission gate.


The shape

applications/main/src/app/api/my-thing/route.ts

import { NextRequest, NextResponse } from "next/server";
import { PERMISSIONS } from "@/lib/permissions";
import { requirePermission } from "@/lib/require-permission";

/**
 * One sentence on what this returns.
 * Requires `processes:audit` — say which, and why that one.
 */
export async function GET(request: NextRequest) {
  const denied = await requirePermission(request, PERMISSIONS.PROCESSES_AUDIT, {
    message: "processes:audit permission required",
  });
  if (denied) return denied;

  // …
  return NextResponse.json({ ok: true });
}

requirePermission returns a response when denied and null when allowed. Return it immediately — do not invert the check.


Middleware has already run

applications/main/src/middleware.ts matches /api/:path* and, before your handler:

  • handles CORS preflight
  • lets /api/auth/login, /api/auth/callback, /api/auth/my-organizations through
  • requires a Bearer token
  • validates the JWT against JWKS
  • checks X-Process-Platform-Organization matches org_id in the token

So your handler can assume an authenticated, org-scoped caller. It still must check which permission.

Two exceptions to know about:

Optional-auth routes. GET /api/process/{id} and the file-download route are allowed through without a token, because sharing may grant anonymous access. The handler decides. If you add a route that link-sharing should reach, add it to isOptionalAuthProcessRead.


Choosing the permission

NeedUse
Read one processrequireProcessRead — honors owner, read_all, and sharing
Write one processrequireProcessWrite
Read across processes, no writePROCESSES_AUDIT
Read across processes, adminPROCESSES_READ_ALL — also confers write
TemplatesTEMPLATES_READ (templates are code-owned; there is no write permission)

Prefer processes:audit over processes:read_all for anything read-only. That distinction is the reason audit exists.

For per-process access always use the requireProcessRead / requireProcessWrite helpers rather than checking a permission string — they encode the four-way resolution (owner, admin, assignment, link visibility) that a bare permission check misses.


Adding a permission string

platform/contracts/src/permissions.ts — add to PERMISSIONS and PERMISSIONS_LIST.

Then in Auth0: add it to the API, add it to the relevant organization roles, and assign those roles at org membership level. A permission that exists in code but not in Auth0 is never granted to anyone.


Filtering field writes

If your route accepts context updates, filter to permitted keys before merging:

import { filterPermittedInputs } from "@/lib/input-permissions";

Dropping unauthorized keys silently is the established behavior — do not error, since a partial write by a partially-permitted actor is a normal case.


Writing process context

Always go through appendStepContextAudit, never assign to process.context directly:

appendStepContextAudit(process, principal.userId, stepKey, updates);

Use SYSTEM_STEP_CONTEXT_USER_ID when the write is automation rather than a person. A direct assignment leaves no audit trail and makes the change invisible to audit tooling.


The OpenAPI spec

applications/main/src/lib/openapi.ts is a hand-maintained spec powering the Swagger UI at /docs/api. It already drifts from the routes.

Do not treat it as a contract. Update it if you are adding something a consumer needs to discover, but the route implementation is the source of truth. See known issues.


Verify

npx tsc --noEmit

Then confirm:

  • no token → 401
  • valid token without the permission → 403
  • valid token with it → 200