Project · Lifebot V2lifebot-v2 — Low-Level Design
type lldstatus activelifebot · health · zero-knowledge · crypto
Components
| Area |
Path |
| Crypto envelope (TS, libsodium injected) |
packages/crypto/src/{envelope,vault}.ts |
| Node libsodium loader |
packages/crypto/src/loaders/node.ts |
| Python verifier (PyNaCl, cross-language) |
services/verify/verify.py |
| Sync engine + aggregation + coach |
packages/client/src/{engine,aggregate,coach}.ts |
| Design tokens + primitives |
packages/ui/{tokens.json,tailwind-preset.cjs,src/} |
| ZK sync API |
apps/api/app/{main,auth,db,sync,models}.py, routers/{vault,sync,health}.py |
| DB schema + RLS |
apps/api/app/schema.sql |
| Dashboard SPA |
apps/web/src/{App.tsx,sodium.ts} |
| Control plane (vendored SaaS-Boilerplate) |
apps/platform/ |
| RN scaffold |
apps/mobile/{App.tsx,src/} |
| Prod compose + tunnel + backups |
docker-compose.prod.yml, ops/backup.sh, docs/stage2-linux-runbook.md |
Data model
lifebot DB (data plane; role lifebot_app, FORCE RLS on app.user_id GUC per-transaction)
users(id uuid, iss, sub, email) -- identity keyed on immutable (iss,sub), UNIQUE
vaults(user_id PK→users, v, kdf jsonb, salt, wrapped_key jsonb, verifier jsonb)
records(user_id, dedupe PK, vv jsonb, lamport, device, tombstone, ct, seq bigint)
-- ct = opaque "nonce.ct" blob; dedupe = HMAC-derived, server can't recover record id
platform DB (control plane; Drizzle migrations from apps/platform — separate database by design)
Crypto envelope (pinned in spec/envelope.md)
- Argon2id (
ops=3, mem=64MiB, 16B salt) → master key; wraps CSPRNG 32B data key (XChaCha20, AAD lifebot/wrap/v1).
verifier seals the constant lifebot/verify/v1 — wrong password fails the AEAD tag, no oracle.
- Records: XChaCha20-Poly1305-IETF, 24B random nonce (multi-device safe), AAD-authenticated header.
- Same bytes on every platform: libsodium sumo (JS/WASM), PyNaCl (server verify), RN native later.
Configuration (API)
| Env |
Purpose |
ADMIN_DATABASE_URL / DATABASE_URL |
schema apply (admin) / runtime as non-owner lifebot_app |
AUTH_DEV_BYPASS |
local only; prod sets 0 |
CLERK_JWKS_URL / CLERK_ISSUER / CLERK_AUDIENCE |
JWT verification (RS256, JWKS) |
CORS_ORIGINS, REDIS_URL, OTEL_EXPORTER_OTLP_ENDPOINT |
edges, cache, telemetry |
Interfaces (HTTP)
| Method + path |
Auth |
Purpose |
GET /health |
none |
liveness (DB ping) |
GET/PUT /v1/vault |
Bearer JWT |
fetch/store encrypted key material |
POST /v1/sync |
Bearer JWT |
push encrypted deltas; version-vector conflict resolution |
GET /v1/sync?since= |
Bearer JWT |
pull ciphertext deltas by server seq cursor |
Sequences
sequenceDiagram
participant C as Client (web/RN)
participant I as Clerk
participant A as API (ZK)
C->>I: OIDC sign-in (Google SSO)
I-->>C: JWT
C->>A: GET /v1/vault (Bearer)
A-->>C: salt + wrapped key + verifier (no key)
Note over C: Argon2id(password, salt) → unwrap data key<br/>wrong password = AEAD tag failure
C->>C: encrypt metrics (XChaCha20, AAD)
C->>A: POST /v1/sync {dedupe, vv, lamport, ct}
A-->>C: actions (created/duplicate/stale/conflict) + cursor
C->>C: decrypt + aggregate + render (client-side only)
Sync conflict rules (apps/api/app/sync.py, pure + unit-tested)
- Version-vector compare: equal→duplicate · dominates→update · dominated→stale · concurrent→conflict.
- Conflicts resolve deterministically LWW by
(lamport, device); replicas converge order-independently.
- Deletes propagate as tombstones; server bumps
seq (pull cursor) on every write.
Tests (required)
npm run spike — TS↔Python crypto interop + nonce uniqueness + AAD tamper + sync harness (8 checks).
apps/api/tests/test_sync.py — conflict-logic unit tests.
packages/client/test/integration.ts — E2E vs live API: vault create, 24 metrics round-trip, wrong-password reject.
- RLS negative test: wrong tenant GUC sees 0 rows (manual, promote to CI).