Skip to content

ADR-003: Personal plane authentication

  • Status: Proposed
  • Date: 2026-05-29
  • Deciders: Keepin’ Tracks engineering
  • Supersedes:
  • Superseded by:

The personal product runs on its own Cloudflare isolation plane (personal-api, personal-web, personal-db, personal-sessions KV). Users must sign in without sharing auth infrastructure with the business plane or marketing site.

Architecture (ARCHITECTURE.md) requires:

  • Magic link (MVP) and passkeys (later)
  • D1 for users and sessions; KV for session cache and rate limits
  • Cookie personal_sessionhttpOnly, Secure, SameSite=Lax
  • Single-user tenancy (user_id on every row; no org_id)

We evaluated three implementation paths for Cloudflare Workers + D1:

OptionProsCons
Better AuthFeature-rich; D1 adapter existsLarge bundle; Node/API surface not always Worker-native; magic-link + cross-subdomain cookies need careful config
Auth.jsPopularPrimarily Next.js-oriented; Workers adapter immature for our Hono SPA + API split
Custom Hono routesFull control; minimal deps; Web Crypto native; easy to test pure functions; mirrors future business stackWe own security maintenance

Implement custom Hono auth routes in apps/personal-api using Web Crypto (SHA-256 token hashing, crypto.randomUUID IDs). No shared packages/auth — business will copy the pattern with its own secrets and cookie name.

  • Opaque session ID in personal_session httpOnly cookie
  • D1 sessions table is source of truth
  • KV cache (session:{id}) with TTL aligned to session expiry; invalidated on logout
  • 30-day sliding expirationlast_seen_at and expires_at updated on authenticated requests
  1. POST /auth/magic-link — validate email, rate-limit (KV), upsert user, store hashed token in D1, send email
  2. User clicks link → GET /auth/magic-link/verify?token=… — constant-time hash lookup, single-use (used_at), create session, set cookie, redirect to WEB_ORIGIN
  3. Token TTL: 15 minutes
  4. Rate limit: 5 requests / hour / email (KV counter)
EnvironmentMechanism
Local devConsole log + GET /dev/magic-link/latest (gated to ENVIRONMENT=development)
Staging / productionCloudflare Email Service (binding TBD; sender domain must be verified)
EnvironmentWebAPICookie Domain
Localhttp://localhost:5173http://localhost:8787 (Vite proxy — same origin to browser)omitted
Productionhttps://personal.keepintracks.comhttps://api.personal.keepintracks.com.personal.keepintracks.com

CORS allowlist uses WEB_ORIGIN with credentials: true. SPA calls API with credentials: 'include'.

  • CSRF: SameSite=Lax on session cookie; state-changing routes use POST; magic link verify is GET but single-use and short-lived
  • Token storage: versioned SHA-256 digests via @keepintracks/crypto (see ADR-004); raw token in URL only
  • Constant-time compare via hashed lookup (no early exit on prefix)
  • Secrets: SESSION_SECRET (personal-api only, via wrangler secret); never shared with business-api
  • No client secrets — SPA never sees session ID except via httpOnly cookie
Table / fieldPurposeRetention
users.emailAccount identityUntil account deletion
users.created_at, updated_atAuditUntil account deletion
sessions.user_agent, sessions.ip_addressSecurity / abuse detectionSession TTL (30 days sliding)
sessions.expires_at, last_seen_atSession lifecycleAuto-purged on expiry
magic_link_tokens.emailDelivery audit24h after expiry (cleanup job later)

User rights (MVP stubs):

  • Export: GET /me/export — user id, email, timestamps (Law 25 export path)
  • Deletion: DELETE /me — cascades sessions, magic links, user row
  • Logout: immediate session invalidation in D1 + KV

Auth requires connectivity for magic link delivery and session bootstrap. The SPA caches the last known session user in memory only; offline product features (Phase 2+) will use separate local credentials — not documented here.

Add passkey_credentials table and /auth/passkey/* routes in a follow-up ADR amendment. Session cookie model unchanged.

  • Worker-native, minimal bundle, full D1/KV control
  • Clear template for business-plane auth (separate cookie, secrets, migrations)
  • Testable pure crypto and validation helpers
  • Team maintains auth code (no vendor SLA)
  • Email Service integration required before production magic links
  • Cloudflare Email Service binding and verified sender domain
  • Passkey (WebAuthn) support
  • Account export / deletion endpoints
  • Session cleanup cron (expired rows)
  • Business-plane ADR mirroring this pattern

Cryptographic algorithms and post-quantum roadmap: ADR-004: Crypto agility and post-quantum.