1## Context23All requests are authenticated today by a single bearer token stored in `config/tally.toml`. There is no `users` table. The web UI is a server-rendered Go app, so cookie sessions are simpler than JWTs.45## Goals / Non-Goals67**Goals:**8- Per-user identity on every request9- Sessions that survive browser restarts when the user opts in10- Basic brute-force protection without external infrastructure1112**Non-Goals:**13- SSO / OAuth providers (follow-up)14- Personal access tokens for the API (separate change)15- Multi-factor authentication1617## Decisions1819- **argon2id over bcrypt**: argon2id is memory-hard and is the OWASP 2024 recommendation; `golang.org/x/crypto/argon2` is already vendored.20- **Opaque server-side sessions over JWT**: we need instant revocation and already have Postgres; a 32-byte random session id in an `HttpOnly; Secure; SameSite=Lax` cookie keeps the client dumb.21- **Lockout counter in Postgres, not Redis**: login volume is tiny; avoiding a new dependency wins.2223## Risks / Trade-offs2425- [Lockout can be abused to deny service to a known email] → Lockout is time-boxed to 15 minutes and does not reveal whether the account exists.26- [Session table grows unbounded] → Nightly job deletes sessions idle past expiry.