clavitor/docs/COMMERCIAL-ONBOARDING.md

208 lines
7.5 KiB
Markdown

# clavitor — Commercial Onboarding Architecture
*Last updated: March 2026*
---
## Overview
The commercial onboarding flow connects Stripe payments to vault provisioning without violating zero-knowledge principles. The account system knows billing metadata — who paid, how many vaults they have, when they expire. It never touches vault contents, keys, or encrypted data.
Self-hosted users are unaffected. The commercial code path only activates when `vault_meta` exists in a vault's SQLite.
---
## Vault Filename Scheme
The vault filename is derived from the PRF master secret:
```
Master (32 bytes) from PRF
L3 = bytes[0..32] (full 32 bytes)
L2 = bytes[0..16] (first 16 bytes)
L1 = bytes[0..8] (first 8 bytes)
filename = bytes[0..4] → base64url (no padding) = 6 chars
```
Filename format: `clavitor-AbCdEf` (prefix + 6 base64url chars, no extension).
- 4 bytes → 2^32 ≈ 4 billion unique vaults
- Reveals only 4 of the 8 L1 bytes — not enough to derive any key
- Deterministic: same hardware key always produces the same filename
---
## The Account System
A new service. Go + SQLite. Runs on HQ (Zürich). Handles billing, not security.
### What it stores
```
accounts:
email TEXT PRIMARY KEY
stripe_customer_id TEXT
created_at TEXT
vaults:
vault_id TEXT -- the "AbCdEf" part
account_email TEXT -- FK to accounts
region TEXT -- which POP
created_at TEXT
```
### What it does NOT store
- Encryption keys (L1, L2, L3)
- Vault contents or metadata
- Session state
- Anything that touches the zero-knowledge guarantee
### API
```
POST /checkout Stripe Checkout session → redirect to Stripe
POST /webhook/stripe Stripe confirms payment → create account
GET /vault/{id}/status Is this vault still paid? (called by POP on expiry)
POST /vault/create POP registers new vault against account
POST /vault/{id}/delete POP notifies vault deleted, frees capacity
```
---
## Consumer Flow ($12/year)
### Purchase
1. User visits clavitor.com → enters email → redirected to Stripe Checkout
2. Stripe collects payment ($12/year)
3. Stripe webhook → account system creates account (email + Stripe customer ID)
4. User shown: "Pick your region" → selects a POP
### Vault Creation
5. User lands on POP (e.g. `eu.clavitor.com`) → enters email
6. Registers passkey → WebAuthn PRF fires → 32-byte master derived
7. POP takes first 4 bytes → base64url → vault filename `clavitor-AbCdEf`
8. POP calls account system: `POST /vault/create` with email + vault_id
9. Account system checks: does this account have capacity? (consumer = max 1 vault)
10. Yes → records vault, responds with `expires_at`
11. POP creates SQLite file `clavitor-AbCdEf` with `vault_meta` row (account_email, expires_at)
12. Vault is live. User is unlocked. Registration = unlocked.
### Every Request (hosted vault)
1. Bearer token carries L1 (8 bytes)
2. POP derives filename from first 4 bytes → opens SQLite
3. Checks `vault_meta.expires_at` — if past, calls home (see Expiry below)
4. If valid → process request as normal
### Expiry & Renewal
- No per-request call to HQ. The vault checks its local `expires_at`.
- When `expires_at` is reached, the POP calls `GET /vault/{id}/status` on HQ.
- HQ checks Stripe subscription status → responds with new `expires_at` or "expired."
- Renewed → POP updates `expires_at` locally, proceed.
- Not renewed → 402 Payment Required. Vault data is intact but inaccessible.
### Vault Deletion
- Initiated through account management on clavitor.com (not the vault UI).
- Account site → strong auth → user deletes vault.
- Account system calls POP → vault SQLite deleted → vault record removed from account → capacity freed.
- 30-day money-back guarantee, no questions asked.
---
## Self-Hosted (Free, Elastic License)
No `vault_meta` table. No expiry check. No call home. No account system interaction. The vault binary works exactly as it does today. The commercial code path is inert.
---
## Account Management (clavitor.com)
Central control plane for billing and vault lifecycle. Separate from any vault UI.
- View account, payment status, invoices
- See owned vaults (vault ID + region)
- Delete a vault
- Manage Stripe subscription (cancel, update payment method)
- Strong auth required (passkey — same hardware key the user already owns)
---
## Hosted POP Architecture
The vault binary is the same binary self-hosters run. It has no management API, no delete endpoint, no inbound control surface. This is by design — self-hosters must never be exposed to hosted infrastructure concerns.
Hosted POPs run two processes:
```
clavitor — port 1984, public internet
Dumb encrypted storage engine. Identical to self-hosted binary.
clavitor-mgmt — Tailscale network only, no public interface
Management sidecar. Handles commands from HQ.
```
### Why Two Processes
The clavitor binary cannot have a delete endpoint. If it did, every self-hosted instance would have one too. Separating management into a sidecar means:
- The clavitor binary stays unchanged for self-hosters
- The management surface is only reachable via Tailscale (not the public internet)
- Self-hosters never install, run, or know about the sidecar
### Tailscale for Management
Tailscale provides the secure channel between HQ and POP sidecars:
- **Identity-based ACLs:** Only HQ's account system can reach `clavitor-mgmt` on any POP
- **Zero config on new POPs:** `tailscale up` with an auth key, done
- **No public exposure:** The sidecar binds only to the Tailscale interface
- **POPs can't reach each other's mgmt:** ACL policy enforces HQ-only access
```
Tailscale ACL (simplified):
HQ account-system → clavitor-mgmt on any POP ✅
POP mgmt → POP mgmt ✗
Anything else → clavitor-mgmt ✗
```
### Management Sidecar Responsibilities
The sidecar is a small Go binary with a narrow API:
```
POST /vault/{id}/delete Delete a vault SQLite file
POST /vault/{id}/extend Update expires_at in vault_meta
GET /vault/{id}/exists Confirm a vault file exists
GET /health Sidecar is running
```
It has filesystem access to the vault directory. It reads/writes `vault_meta` and can delete vault files. It never opens or decrypts vault contents.
---
## POP ↔ HQ Communication
| When | Direction | Channel | What |
|------|-----------|---------|------|
| Vault creation | POP → HQ | Tailscale | Validate account, register vault |
| Vault expiry | POP → HQ | Tailscale | Check renewal status |
| Vault deletion | HQ → POP | Tailscale (via mgmt sidecar) | Delete vault file |
| Expiry update | HQ → POP | Tailscale (via mgmt sidecar) | Extend `expires_at` |
No runtime dependency on HQ. POPs operate independently except at creation and expiry boundaries. All management traffic flows over Tailscale — never the public internet.
---
## Open Questions
- **Account management auth:** Passkey on clavitor.com — same authenticator or separate registration?
- **Grace period on expiry:** How many days past `expires_at` before 402? Immediate, or a buffer (e.g. 7 days)?
- **Deletion confirmation:** Sidecar deletes file → responds to HQ → HQ removes vault record. What if the file delete fails? Retry? Tombstone?
- **Future: second copy (read replica):** Noted as planned. Architecture supports it — vault_id is deterministic, same file can exist on two POPs. Sync model TBD.