208 lines
7.5 KiB
Markdown
208 lines
7.5 KiB
Markdown
# vault1984 — 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: `vault1984-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 vault1984.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.vault1984.com`) → enters email
|
|
6. Registers passkey → WebAuthn PRF fires → 32-byte master derived
|
|
7. POP takes first 4 bytes → base64url → vault filename `vault1984-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 `vault1984-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 vault1984.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 (vault1984.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:
|
|
|
|
```
|
|
vault1984 — port 1984, public internet
|
|
Dumb encrypted storage engine. Identical to self-hosted binary.
|
|
|
|
vault1984-mgmt — Tailscale network only, no public interface
|
|
Management sidecar. Handles commands from HQ.
|
|
```
|
|
|
|
### Why Two Processes
|
|
|
|
The vault binary cannot have a delete endpoint. If it did, every self-hosted instance would have one too. Separating management into a sidecar means:
|
|
|
|
- The vault 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 `vault1984-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 → vault1984-mgmt on any POP ✅
|
|
POP mgmt → POP mgmt ✗
|
|
Anything else → vault1984-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 vault1984.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.
|