Components
Product root: ~/Terraform/SAAS/SAAS-Lifebot (ixartz SaaS Boilerplate).
| Area | Path |
|---|---|
| Schema | src/models/Schema.ts + Drizzle migrations |
| DB client | src/libs/DB.ts |
| Env | src/libs/Env.ts (APP_SECRET = ingest HMAC pepper only) |
| Repositories | src/features/lifebot/db/* (only health DB entry) |
| Crypto server | src/features/lifebot/crypto/* (HMAC, seal-to-pubkey, ingest-merge KDF) |
| Vault client | src/features/lifebot/vault/* (keygen, unlock, decrypt) |
| Ingest normalize | src/features/lifebot/ingest/* (port of Python handlers) |
| Coach context | src/features/lifebot/coach/* (client-side aggregate) |
| Health UI | src/app/[locale]/(auth)/dashboard/health/*, src/features/health/* |
| Settings | src/app/[locale]/(auth)/dashboard/settings/* |
| APIs | src/app/api/** |
| Ingest routes | src/app/api/ingest/health/route.ts, hevy/route.ts |
| Middleware | src/proxy.ts — public /ingest/*, protect dashboard/api |
| Deploy | Dockerfile, docker-compose.yml, docs/HOME-SERVER.md |
Data model
user_settings
ownerId PK
ingestKeyHash UNIQUE, ingestKeyPrefix
publicKey, encryptedPrivateKey, kdfSalt, kdfParams
consentHealthAt, consentAiAt, retentionDays
timestamps
daily_metrics
id, ownerId, date, name?
sealedPayload -- display JSON sealed to user pubkey
mergeStateCiphertext? -- openable only with ingest-derived key
UNIQUE(ownerId, date, name)
workouts
id, ownerId, date, source, externalId
sealedPayload
UNIQUE(ownerId, source, externalId)
goals
ownerId, metric, sealedPayload
advice (optional store)
ownerId, date, sealedPayload
audit_events
ownerId, action, ipHash?, meta (non-sensitive), createdAt
Rule: no plaintext health values/units/targets/advice text in any column.
Configuration
| Variable | Where | Purpose |
|---|---|---|
CLERK_SECRET_KEY / NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
server/client | Auth |
DATABASE_URL |
server | Postgres (compose) or PGlite URL in dev |
APP_SECRET |
server | HMAC pepper for ingest keys only — not health DEK |
NEXT_PUBLIC_APP_URL |
client | Canonical HTTPS/tailnet URL |
ANTHROPIC_API_KEY |
prefer client / optional BFF | Coach — avoid server if possible |
Interfaces (HTTP)
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/stats?days= |
Clerk | Sealed blobs for session user |
| POST | /api/goals |
Clerk | Client-sealed goal body |
| POST | /api/vault |
Clerk | Store publicKey + wrapped private key |
| POST | /api/workouts |
Clerk | Client-sealed workouts (Hevy client path) |
| GET | /api/export |
Clerk | Sealed export package |
| POST | /api/delete-data |
Clerk | Wipe owner rows + vault |
| POST | /api/demo/seed |
Clerk | Accept sealed demo rows |
| POST | /api/settings/ingest-key |
Clerk | Rotate key; return raw once |
| POST | /ingest/health |
x-lifebot-key |
Normalize → merge → seal |
| POST | /ingest/hevy |
x-lifebot-key |
Same |
Ignore client-supplied ownerId / userId for authorization.
Sequences
Vault create + unlock
sequenceDiagram participant U as User browser participant A as Next.js participant DB as Postgres U->>U: generate keypair, Argon2 wrap private key U->>A: POST /api/vault (publicKey, encryptedPrivateKey, salt) A->>DB: store for ownerId U->>U: unlock: password → KEK → private key in memory U->>A: GET /api/stats A->>DB: sealed rows for ownerId A->>U: ciphertext only U->>U: unseal → render KPIs/charts
HAE ingest (admin-blind at rest)
sequenceDiagram participant P as Phone HAE participant A as Next.js participant DB as Postgres P->>A: POST /ingest/health + x-lifebot-key + JSON A->>A: HMAC verify key → ownerId + publicKey A->>A: derive ingestMergeKey from key (ephemeral) A->>DB: load mergeStateCiphertext A->>A: open merge state, merge samples, re-seal merge state A->>A: seal display payload to user publicKey A->>DB: write sealedPayload + mergeState Note over A,DB: Admin has neither ingest key nor private key
Idempotent merge
Port CUMULATIVE + watermark logic from Bobs-lifebot/lambdas/ingest/handler.py. Merge
state encrypted under ingest-derived key; dashboard payload sealed to user public key.
Tests (required)
- Owner A cannot read B’s rows (repository + API).
- Sealed payload not decryptable with
APP_SECRETalone. - Wrong vault password fails unlock.
- Ingest wrong key → 401; right key writes only that owner.
- Logs contain no metric values / bodies.
Related
Compiled from
wiki/projects/SAAS-Lifebot/LLD.md · git is the source of truth