Documentation
Security
Authentication and Access

Authentication and Access

Passwordless email login

Epics & Stories uses a passwordless email OTP (one-time password) flow:

  1. User enters their email address on the login page.
  2. A 6-digit code is sent to that email via SMTP.
  3. User enters the code — if valid, a session cookie is set.
  4. 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 attributeValueWhy
HttpOnlytruePrevents JavaScript access
SecuretrueHTTPS only
SameSiteStrictCSRF protection

Sessions expire after a period of inactivity. Users must re-authenticate once the session expires.

OTP security properties

PropertyBehaviour
Code length6 digits
ExpiryShort-lived (set by server-side TTL on the KV entry)
Max attemptsLimited — too many wrong attempts invalidates the code
Rate limitPer-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:

StateDescription
UnclaimedNo owner yet — any authenticated user who opens it can claim it
OwnedHas an owner; only owner + invited collaborators can access
InvitedOwner has added your email; you have access per your plan's collaboration rights

Access hierarchy

  1. Owner — full access, can invite collaborators, can manage phases
  2. Invited (Pro board owner) — edit access
  3. Invited (Free board owner) — view-only access
  4. 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:

  1. Extract and validate the session cookie.
  2. Verify the session exists in KV.
  3. Check the user's board access level from the board metadata KV record.
  4. 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