Project · Saas Lifebot

SAAS-Lifebot — Low-Level Design

type lldstatus activelifebot · saas · nextjs · drizzle

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_SECRET alone.
  • Wrong vault password fails unlock.
  • Ingest wrong key → 401; right key writes only that owner.
  • Logs contain no metric values / bodies.
Compiled from wiki/projects/SAAS-Lifebot/LLD.md · git is the source of truth