Different Hunger

Security

Adding an endpoint

The checklist for shipping a new API route without opening a hole. Four questions, then the gate decides.

Adding an endpoint

Four questions. Answer them before writing the handler, not after the audit fails.

1. Who is allowed to call this?

AnswerWhat to write
One brand's peopleif (!(await requireBrandAccess(slug, req))) return denied();
Any studio operatorif (!(await requireOperator(req))) return denied();
A payment processorVerify the signature. A shared secret in a query string is not a signature.
Genuinely anyoneGo to question 2.

2. If public — which of the four reasons is it?

Add it to PUBLIC_ROUTES in lib/security/policy.ts with self-capture, signed-webhook, public-content or stateless-compute.

If none of the four fits, it is not public. This is the point of the list: writing the line forces the reason to exist, and the reason is reviewable later by someone who was not in the room.

Watch this one. NEVER_PUBLIC patterns — anything under contacts/, admin/, brand/[slug], hq/brand — override the list. Adding one of those to PUBLIC_ROUTES fails the audit rather than taking effect.

3. What does it return?

Name every field. select("*") is how a live third-party API key ended up on the public internet: the column was added to the table months after the route was written, and the route had no opinion about it.

Wrap any response containing brand config in redactSecrets(). It is cheap, and it protects the secret somebody adds next year.

4. What does it write?

  • Does it send anything to a human? Derive the recipient from the record, never from the request body.
  • Does it move money or change access? It needs an operator, and it needs to be idempotent.
  • Does it accept an id from the caller? Confirm that id belongs to the caller before acting on it.

Then run the gate

npm run security:gate

Exit 0 means every route is either guarded or declared. Exit 1 prints the ones that are neither, and the build stops.

The audit matches on real authorisation primitives — requireBrandAccess, requireOperator, isBrandOwner, a signature check, a bearer comparison. A comment saying the route is safe does not satisfy it, deliberately.

After a route changes shape

npm run security:posture

Regenerates the numbers on the public page from the live system. Do this before a release that adds or removes endpoints, so the outward-facing document stays true.