launch-quickly
Decisions

0003 — TenantContext is branded

No code outside one module can produce one, not even by writing an object literal with the right shape. And why middleware is not a security boundary.

Context

"Did this code path check authorization?" is a question you can only answer by reading every code path. Most frameworks answer it with middleware, which is attractive because it is one place — and wrong, because it is the wrong place.

Decision

TenantContext carries a brand: a unique symbol that is declared and never given a value. Nothing outside src/lib/auth/context.ts can produce one — not even by writing an object literal with every field correct. Every query and mutation takes one as its first parameter.

which makes "I forgot to check auth" a compile error rather than a security incident.

Row-level security (0002) makes the database refuse. This makes the call not compile. They are the same argument at two different rungs of ADR-0001.

Rejected: middleware as the boundary

src/proxy.ts is redirect UX and nothing else. Next 16 renamed the convention from middleware to proxy for precisely this reason: it is a network boundary in front of the app, not a security layer inside it.

The matcher lists public paths, so a new route is private by default. That is not a stylistic preference — the previous version enumerated protected routes and shipped a hole the moment one was missed. It matched with Array.includes() against the literal string '/dashboard/:path*', which meant every nested route under it was open.

A second incident from the same file: a Stripe webhook received a 307 to /sign-in, because machine callers were not exempted. Unit tests call route handlers directly, so nothing between the code and production would have caught it.

The carve-out, and why it is honest

createTenantContext runs under withSystem, which bypasses RLS. That looks like a contradiction and is not: resolving which tenant you are in necessarily happens before tenant scoping exists. A withTenant query there would be circular, and an RLS policy would filter out the very row being read.

Writing that down matters more than the exemption itself. An undocumented exception to a security rule is indistinguishable from a bug.

On this page