Hosted Auth Design
Status: design. Target: ADR-0026.
The July 2026 P0 foundation uses product-local Firebase email/password auth with email verification, recovery, persistent browser sessions, and sign-out. This is the implemented first slice, not proof of a production journey. The passkey and magic-link system below remains the target upgrade; none of it is claimed as built by the foundation.
Decision summary
- Passkeys (WebAuthn) as the primary auth method.
- Magic-link email as the fallback for first sign-in and passkey loss.
- No passwords. Ever.
- Session:
HttpOnlycookie, 30-day rolling, bound to tenant id.
Why passkeys
Founder segment is technical-enough to register one, security-aware enough to value not having a password, and small enough that passkey-recovery support cost is bounded. Founders mostly use laptops with platform authenticators (Touch ID, Windows Hello).
Sign-up flow
- Stripe checkout completes (see
BILLING.md) and emits a hosted webhook with{ email, tenant_slug, plan }. - Hosted control plane provisions the tenant container + volume
(see
MULTI_TENANT.md). - Founder receives a magic-link email: "Set up
.Lycato". - Click → 1-time link consumes (15-min TTL). Founder lands on the passkey registration page. Skip is allowed; passkey can be registered later from Settings.
- Session cookie issued. Founder enters the cockpit.
Sign-in flow
1. Founder visits <slug>.Lycato
2. Cockpit shows: [ Sign in with passkey ] [ Email me a link ]
3a. Passkey path: WebAuthn assertion via browser; server validates
signature against stored credential; session cookie issued.
3b. Magic-link path: email entered; one-time link sent (15-min TTL,
one use); click issues session cookie.
4. Cookie scoped to <slug>.Lycato (not Lycato) to keep tenants
isolated at the cookie-jar level too.
Lost passkey recovery
- Magic-link email → "Manage passkeys" page.
- Founder can register a new passkey + revoke the old.
- No support-driven account recovery in alpha. If founder loses
email access too, the recovery path is "restore from your
founder-owned GitHub repo on a fresh tenant" (see
BACKUPS.md).
Cofounder / reviewer access (Team plan)
- Founder invites by email from Settings.
- Invitee gets magic-link → registers their own passkey.
- Reviewer sessions are scoped: read all, approve all, cannot rotate secrets or invite others.
- Reviewer count gated by plan (Team: ≤5).
Session cookie shape
- Name:
co_md_sess - Attributes:
HttpOnly; Secure; SameSite=Lax; Path=/ - Lifetime: 30-day rolling (renewed on every request)
- Value: opaque session id; sessions stored server-side in the control plane DB
- Logout:
DELETE /auth/sessioninvalidates server-side; clears cookie
Cross-tenant isolation
- Cookies scoped to subdomain → physical isolation.
- Server validates
cookie.tenant_id === request.tenant_idon every request. - Mismatch returns 404 (not 403) to avoid revealing tenant existence to scanners.
Rate limits
| Endpoint | Limit |
|---|---|
POST /auth/magic-link |
5/email/hour |
POST /auth/passkey/assertion |
20/cookie/minute |
| Session-checked routes | 600/cookie/minute |
What's intentionally absent
- SSO / SAML. Defer to v1.x enterprise.
- Per-route 2FA escalation. Passkey is already strong; revisit if high-risk approval flow asks for it.
- OAuth as primary. Founder owns the bank, not Google.