6.3 KiB
Helios — design spec
2026-08-18 · status: approved pending Marcus's review
What
Helios is a self-hosted, MIT-licensed web app: a FOSS alternative to Illume Labs / Superpower. One Docker container per user (or household). The pitch: your health data lives on your server, and the AI runs on your key.
v1 features:
- Connect wearables with the user's own credentials: Oura, Whoop, Fitbit (official APIs), Garmin (unofficial library, clearly labeled).
- Upload lab PDFs → LLM extraction → user reviews/confirms → normalized biomarkers with units, reference ranges, and flags.
- Log meals by photo or text → LLM macro estimate → editable card.
- Grounded AI chat: tool-calls into the local DB, so only relevant data slices are sent to the LLM — never a full dump.
- Daily brief generated at a user-chosen hour from the last 24 h + 30-day trends + flagged labs.
- Dashboards: Today (brief + cards), Sleep, Training, Labs, Nutrition, Settings.
Non-goals for v1: multi-user SaaS, mobile app (planned fast-follow for HealthKit/Health Connect sync), Apple Health ingestion (needs the companion app), medical advice claims of any kind.
Architecture
API-first monolith. The web UI is a pure client of a typed REST API — that API is the contract a future mobile app reuses unchanged.
Web UI (Vite + React SPA)
Today/brief · Chat · Sleep · Train · Labs · Nutrition · Settings
│ typed REST (Zod-validated)
Server (Bun + Hono, one process)
vendor pollers (in-process cron, per-token, with backfill)
lab PDF → LLM extraction → review queue
meal photo/text → LLM macro estimate
chat orchestrator (typed tools over the DB)
daily brief generator (cron)
│
SQLite (single file, WAL, Drizzle ORM) + uploads dir on one /data volume
LLM: any OpenAI-compatible endpoint — OpenRouter documented default,
Ollama/LM Studio for zero-third-party operation
Stack rationale: TypeScript end to end (shared types between API, UI, and LLM tool schemas); SQLite over Postgres because a per-user instance needs one-file backup and zero ops (Open WebUI ships the same way); SPA over Next.js because an API-first self-hosted app gains nothing from SSR and the SPA keeps the mobile-later contract honest.
health-api (marcus/health-api) is reference only: its lab
normalization (normalize.ts, units.ts) and schema shapes inform clean
re-implementations. No code copied with Marcus-specific assumptions, no
personal data, MIT throughout.
Data model (Drizzle/SQLite)
settings— LLM base URL + key, vendor tokens, brief hour. Secret columns encrypted (see Security).daily_metrics— per-day, per-source rollups: steps, resting HR, HRV, active calories, readiness/recovery scores.sleep_sessions— start/end, stage durations, scores; raw vendor JSON retained in arawcolumn.workouts— type, duration, load metrics; raw JSON retained.lab_draws+biomarkers— draw metadata; marker slug, value, normalized unit, reference range, flag.meals— timestamp, photo path, macro estimate, LLM confidence, user-edited flag.chat_threads+chat_messages.briefs— generated markdown + input snapshot.sync_state— per-connector cursor/watermark for incremental polls.
Connectors
| Vendor | Path | User setup |
|---|---|---|
| Oura | official API | paste personal access token |
| Whoop | official API | register own free dev app, paste client id/secret (documented 5-min flow) |
| Fitbit | official API | same own-app flow as Whoop |
| Garmin | unofficial garmin-connect lib |
username/password; labeled "may break without notice" |
Pollers run on an in-process cron; first connect triggers historical
backfill; sync_state makes polls incremental. Every connector normalizes
into the tables above and keeps raw vendor JSON.
Labs: PDF upload → LLM extraction prompt → review/confirm screen —
nothing writes to biomarkers until the user approves the parsed values.
Trust in medical numbers requires the human check.
Nutrition: photo or free-text → LLM estimates items + macros (Open Food Facts as grounding where possible) → editable card; edits stored, model estimate retained for calibration.
AI layer
- Provider abstraction: base URL + key + model id; anything OpenAI-compatible works. OpenRouter is the documented default; Ollama is the fully-local option.
- Chat tools (typed, Zod-schema'd):
query_sleep,query_metrics,query_workouts,query_labs,query_meals,log_meal. Tools return bounded slices (date-ranged, capped row counts). - Daily brief: cron at the user's hour; prompt assembles yesterday's data,
30-day trends, and out-of-range biomarkers; output stored in
briefsand rendered on Today. - Honest privacy wording everywhere (README + Settings): chat context is sent to the LLM endpoint the user chose. Only Ollama-class local endpoints mean zero third parties.
Security
- Auth: single password (argon2id), rate-limited login, httpOnly session cookie. Passkeys are a later upgrade.
- Secrets at rest: AES-256-GCM column encryption; key generated at first
boot into the /data volume (
secret.key). - The SQLite file itself is not app-encrypted in v1 (bun:sqlite lacks SQLCipher); docs prescribe disk encryption + encrypted backups. Column crypto covers the credential-theft blast radius.
- No telemetry, no update phone-home. Outbound traffic only: chosen LLM endpoint + connected vendor APIs.
- CSP on the SPA; uploads served with safe content-type handling.
Testing
- Vitest: unit normalization (units/ranges/marker slugs), connector transforms against recorded vendor fixtures, tool-schema round-trips.
- API integration tests over an in-memory SQLite.
- CI: Gitea Actions — typecheck + tests on push.
Deploy
- One Dockerfile (bun base),
docker compose up, single/datavolume. - Caddy reverse-proxy recipe in docs.
- Repo: git.rehbock.xyz/marcus/helios (public). GitHub mirror optional later for discoverability.
Milestones
- Scaffold: repo, CI, Docker, auth, settings (LLM + encryption plumbing).
- First connector (Oura) end to end: poll → normalize → Sleep/Today UI.
- Chat + tools over whatever data exists.
- Labs pipeline (upload → extract → review → dashboard).
- Meals + daily brief.
- Whoop/Fitbit/Garmin connectors; docs site; v0.1 announcement.