# 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: `HttpOnly` cookie, 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

1. Stripe checkout completes (see `BILLING.md`) and emits a hosted
   webhook with `{ email, tenant_slug, plan }`.
2. Hosted control plane provisions the tenant container + volume
   (see `MULTI_TENANT.md`).
3. Founder receives a magic-link email: "Set up <slug>.Lycato".
4. 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.
5. 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/session` invalidates server-side; clears
  cookie

## Cross-tenant isolation

- Cookies scoped to subdomain → physical isolation.
- Server validates `cookie.tenant_id === request.tenant_id` on
  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.
