BYOK key management with swappable encryption. Two functions are the entire swap surface.
File: supabase/migrations/20260305_vault_secrets.sql
Columns: id, owner_type ('user'|'team'), owner_id, key, encrypted_value, category, description, last_rotated_at, created_at, updated_at. RLS: user manages own secrets. Unique on (owner_type, owner_id, key).
File: lib/vault/crypto.ts
AES-256-GCM via Node.js crypto module. Master key from VAULT_MASTER_KEY env var (32-byte hex). Two functions: encrypt(plaintext) and decrypt(ciphertext). This is the entire MasteryOS swap surface.
File: lib/vault/service.ts
getSecret, setSecret, deleteSecret, listSecrets (metadata only), resolveSecret (user → team → env var fallback). Uses Supabase admin client + crypto.
File: app/api/vault/route.ts
GET (list metadata), GET ?key=X&reveal=true (decrypt), POST (encrypt + store), DELETE. Session-only auth — no API key access to vault.
File: types/database.ts
VaultSecret interface matching table schema.
File: components/VaultManager.tsx
Accordion by category (LLM, Infrastructure, Hosting, Email, General). Each row: key name, description, last rotated, masked value. Show/hide toggle, add/edit/delete. Pre-populated suggestions dropdown.
File: components/DashboardContent.tsx
Add VaultManager below WebhookManager in left column.
Update run/analyze routes: resolve API key from vault first, fall back to env var.
const apiKey = await resolveSecret(userId, null, 'ANTHROPIC_API_KEY')
|| process.env.ANTHROPIC_API_KEY
Generate 32-byte hex key, add to .env.local and Vercel env vars.
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
| File | Role |
|---|---|
| lib/vault/crypto.ts | Encrypt/decrypt — the swap surface |
| lib/vault/service.ts | Business logic + BYOK resolution |
| app/api/vault/route.ts | REST API (session auth only) |
| components/VaultManager.tsx | Dashboard UI component |
| lib/auth/credential-vault.ts | Existing stub — replaced by vault service |