Authentication
Sessions
NextAuth with JWT. requireSession() returns either a user or a response to
return — never throws — and binds the board scope as a side effect.
const auth = await requireSession();
if (auth.response) return auth.response;
const denied = await requireFeature("board_memory");
if (denied) return denied;
requireSession must come first. Outside a session, boardId() returns
the default board, so a feature gate placed before it judges the wrong
tenant's plan. This has shipped once; there is a test that fails if the order
is disturbed.
Passwords
Never issued or transmitted. A user row is created with password_hash NULL —
a valid state that cannot sign in — and a single-use link lets them set their
own. Only the hash of that token is stored.
Passkeys
WebAuthn, for sealing rather than sign-in. The decision hash is bound into the challenge, so the assertion attests to the document. Public keys only.
Entitlements
hasFeature checks whether the feature is attached to the board's plan. It
fails open when the database is unreachable — a permission check that
cannot reach the database must not take a feature from a paying customer.
feature.enforced does not gate anything. It is catalogue metadata.