ADR-0009 — Human authentication in V1
Status: accepted for V1, 2026-11-09. Deviation from the brief, which preferred Auth.js with passkeys and OAuth.
Context
Passkeys and OAuth sign-in need registered OAuth applications, a mail sender for verification and recovery, and product decisions about open registration. None of these were available at build time, and the launch has one human user: the owner.
Decision
- Email and password. Passwords are 12–200 characters, hashed with argon2id (19 MiB memory, 2 iterations).
- Sessions are rows in PostgreSQL. The cookie
skills_sessioncarries a 256-bit random value; the database stores only its keyed HMAC. Lifetime 12 hours. - Cookie-authenticated writes to the API require a same-origin
Originheader and a per-session CSRF token inX-CSRF-Token. - Sensitive actions (creating or rotating tokens) require the password to have been entered within the last 15 minutes.
- Login is rate limited per IP and email (8 attempts per 15 minutes). Unknown accounts are verified against a dummy hash so timing does not reveal whether an email exists. Failures are audited.
- Changing the password revokes the user's other sessions.
- Accounts are created by the operator from the server console (
pnpm admin:bootstrap,pnpm admin user create) with a generated initial password that must be changed at first sign-in. Until it is changed, the session cannot use the API. - Recovery is a console command (
pnpm admin recover), which also revokes all of the user's sessions. - The first administrator is bound to the owner by possession of shell access on the server. The email is recorded as verified with the note
operator-attested; no mail is sent.
Planned
TOTP, passkeys (WebAuthn), OAuth sign-in (the auth_identities table is in place), self-service registration with email verification, mailed password reset.
Consequences
- No second factor yet: the owner account is protected by one strong password and by the fact that elevated API tokens are short-lived.
- No open registration: contributors are onboarded by the operator.
- No dependency on an external identity provider.