Architecture
flowchart TB
subgraph Clients [Clients — plaintext exists ONLY here]
RN[React Native app<br/>HealthKit read + aggregate<br/>encrypt on device]
WEB[React dashboard SPA<br/>WebCrypto/libsodium WASM<br/>decrypt + aggregate in browser]
end
IDP[Clerk IdP<br/>identity only]
subgraph Box [Linux box — Docker Compose]
PLAT[Next.js control plane<br/>SaaS-Boilerplate: sign-up, orgs, account]
API[FastAPI zero-knowledge sync API<br/>stores ciphertext + opaque metadata]
PG[(Postgres<br/>lifebot: RLS data plane<br/>platform: Drizzle control plane)]
RED[(Redis)]
WRK[worker]
end
LLM[Gemini API<br/>coach, client-direct]
RN & WEB -->|OIDC JWT| IDP
RN & WEB -->|Bearer JWT + ciphertext| API
RN & WEB -.->|on-device summary| LLM
PLAT --> IDP
API --> PG
API --> RED
CF[Cloudflare Tunnel] --> PLAT & WEB & APIZero-knowledge model
Master password → Argon2id (per-user salt) → master key → unwraps a random data key → XChaCha20-Poly1305 encrypts every record on the client. Server stores: salt, verifier, wrapped keys, ciphertext, opaque HMAC dedupe keys, version vectors — nothing readable. Identity (Clerk) and unlock (master password) are deliberately two secrets: an OAuth token the server sees can never be an encryption key. See lifebot-v2/adr/0001-zero-knowledge-e2ee and lifebot-v2/adr/0002-one-libsodium-master-password-key.
Control plane vs data plane
| Plane | Owner | Can read health data? |
|---|---|---|
www. control plane (Next.js, SaaS-Boilerplate, Clerk, Drizzle, platform DB) |
account, orgs, billing-ready | No — separate DB, no key |
app. dashboard SPA (React) |
render decrypted data | Only on-device after unlock |
api. data plane (FastAPI, lifebot DB, FORCE RLS) |
ciphertext sync | No — ciphertext only |
See lifebot-v2/adr/0003-saas-boilerplate-control-plane.
AI coach
Runs client-side: decrypt → build minimised summary on device → call Gemini directly (scoped short-lived credential; paid tier = not trained on). The Lifebot server is never in the plaintext path. See lifebot-v2/adr/0004-client-side-ai-coach-gemini.
Components
| Piece | Role |
|---|---|
packages/crypto |
envelope: Argon2id, XChaCha20, AAD header; libsodium injected per platform |
packages/client |
API client, sync engine, client-side aggregation, coach — shared web/RN |
packages/ui |
tokens.json + presentational primitives (Tailwind 3.4 preset, NativeWind-compatible) |
apps/api |
FastAPI ZK sync API: /v1/vault, /v1/sync; JWKS verify on (iss,sub); RLS |
apps/web |
dashboard SPA (Vite/React); Clerk gate → master-password unlock |
apps/platform |
vendored SaaS-Boilerplate control plane (Next 16, Clerk, Drizzle) |
apps/mobile |
RN scaffold (typechecks; Expo build deferred to Stage 4) |
GDPR / data protection
- Special-category data is ciphertext server-side; cleartext metadata minimised to routing only.
- Crypto-erasure: destroying the wrapped key renders all ciphertext permanently unreadable (Art. 17).
- Coach disclosure: consented, minimised summary to Gemini under paid-tier no-training terms.
- DR: the wrapped-key/vault tier is the backup crown jewel (ciphertext is worthless without it).
Decisions
- lifebot-v2/adr/0001-zero-knowledge-e2ee
- lifebot-v2/adr/0002-one-libsodium-master-password-key
- lifebot-v2/adr/0003-saas-boilerplate-control-plane
- lifebot-v2/adr/0004-client-side-ai-coach-gemini
Non-goals
- Server-side analytics/search/AI over health data (impossible by design — accepted).
- Org-visible member health data (orgs govern membership/billing only).
- HIPAA (decide applicability before any US health-context onboarding).
Related
wiki/projects/lifebot-v2/HLD.md · git is the source of truth