Authentication and Access
Passwordless email login
Epics & Stories uses a passwordless email OTP (one-time password) flow:
- User enters their email address on the login page.
- A 6-digit code is sent to that email via SMTP.
- User enters the code — if valid, a session cookie is set.
- No password is stored or transmitted.
Session model
Sessions are stored server-side in Cloudflare KV. The session token is held in an HttpOnly cookie.
| Cookie attribute | Value | Why |
|---|---|---|
HttpOnly | true | Prevents JavaScript access |
Secure | true | HTTPS only |
SameSite | Strict | CSRF protection |
Sessions expire after a period of inactivity. Users must re-authenticate once the session expires.
OTP security properties
| Property | Behaviour |
|---|---|
| Code length | 6 digits |
| Expiry | Short-lived (set by server-side TTL on the KV entry) |
| Max attempts | Limited — too many wrong attempts invalidates the code |
| Rate limit | Per-IP and per-email limits on code sends |
Codes are single-use — a successful verification immediately deletes the code from KV.
Board access model
Boards have three access states:
| State | Description |
|---|---|
| Unclaimed | No owner yet — any authenticated user who opens it can claim it |
| Owned | Has an owner; only owner + invited collaborators can access |
| Invited | Owner has added your email; you have access per your plan's collaboration rights |
Access hierarchy
- Owner — full access, can invite collaborators, can manage phases
- Invited (Pro board owner) — edit access
- Invited (Free board owner) — view-only access
- Unclaimed — any authenticated user can access and claim
Claiming a board
The first authenticated user to visit an unclaimed board URL may claim it by confirming ownership. After claiming, the board is owned and behaves according to the owner's plan.
API authentication pattern
All board-scoped API routes use a centralised boardGuard() middleware:
- Extract and validate the session cookie.
- Verify the session exists in KV.
- Check the user's board access level from the board metadata KV record.
- Return the user email and access result to the route handler.
If any step fails, the handler returns 401 (no session), 403 (no board access), or 503 (service unavailable in production).
Webhook authentication
Stripe webhooks cannot use session cookies. The billing webhook endpoint (POST /api/billing/webhook) authenticates using Stripe's webhook signature — the raw request body is verified against the Stripe webhook secret before any processing occurs.
See also
- Auth API — OTP and session endpoints
- Plans and Limits — How collaboration rights relate to plans