clavitor/docs/COMMERCIAL-ONBOARDING.md

7.5 KiB

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

  1. User lands on POP (e.g. eu.clavitor.com) → enters email
  2. Registers passkey → WebAuthn PRF fires → 32-byte master derived
  3. POP takes first 4 bytes → base64url → vault filename clavitor-AbCdEf
  4. POP calls account system: POST /vault/create with email + vault_id
  5. Account system checks: does this account have capacity? (consumer = max 1 vault)
  6. Yes → records vault, responds with expires_at
  7. POP creates SQLite file clavitor-AbCdEf with vault_meta row (account_email, expires_at)
  8. 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.