Six suites pinned the old rule as the rule. Each inversion, and why it is one: `breadth.test.ts` — the case table asserted a refusal reason per set; it asserts a tier and a `physiological` flag now. Every row in it except the empty set used to be a refusal, and each is a number somebody gets to see. Adds the tier boundaries (one pillar, two areas, physiological-absent), a case proving the same pillars produce the same number at `partial` and at `full`, and a block whose only job is the counter-case: the empty verdict still carries a reason and no tier, and a composite whose selected pillars all came back short still refuses with `no_usable_data`. A rule that has stopped refusing anything is a deleted rule, not a graded one. `composite.test.ts` — "requires three domains and one measured physiological domain" asserted the refusal that no longer happens; it now asserts the score and the basis block on the same fixture. The coverage case keeps its fixture and its arithmetic and flips its outcome: two of three used to be a refusal and is a scored account with a real coverage fraction. Adds the one-area case and the zero-pillar counter-case, and the `SCORE_VERSION` floor moves 2 -> 3 because widening who gets a score is exactly the kind of change that must not ship without a version bump. `record.test.ts` — "records nothing when the composite did not resolve" used a two-pillar fixture to prove a below-the-floor day stored nothing. That day has a number now and the table has to keep it, or an account that starts narrow and widens later has a hole where its early days were. The assertion is now that a one-pillar day builds a draft AND reaches the database; the absence case survives on the fixture that genuinely produces no score. `composition-source.test.ts` — three recipes asserted as refused at read time. Two of them score. What the block proves is unchanged — the reader publishes the shared rule's verdict on the reader's own resolved set, so a second copy of the rule would still surface here — but the agreement is over a tier rather than over a refusal. `route.test.ts` (score config) — the two too-narrow refusals become acceptances, because a write that refuses what the read is willing to serve is the two layers disagreeing. The empty selection keeps its 422 and gains the assertions the old cases carried: errorCode, reason, and the sentence. The errorCode driver loses the two selections that no longer 422. `score-refusal-consumer-guard.test.ts` — its non-zero control anchored on `three_domains_required`; it anchors on `no_pillars_selected`. New: `health-score-basis-additive-contract.test.ts` pins that `scoreBasis` is published, is optional on all three carriers, and points at the one shared component — a required field on a cached surface would invalidate every cached payload with nothing failing anywhere. It also pins that the walk does not confuse the composite's basis with the older per-pillar `scoreBasis` that blood pressure uses for its axis, since they share a key. Fixtures: the notice-key assertions derive the method half from `SCORE_VERSION` rather than restating 3. Three fixtures that named `three_domains_required` now name `no_usable_data`, a reason the server can still emit. The settings e2e drove its refusal through a two-pillar selection that now succeeds; it deselects every pillar instead.
HealthLog
Self-hosted, privacy-first health tracking. Your vitals, your devices, your server.
Website · Live Demo · Documentation · iOS TestFlight
Why HealthLog
HealthLog is a self-hosted health tracking app (PWA + native iOS client) that runs from a single docker compose up on a NAS, homelab, or small VPS. It brings weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications onto one timeline — synced from the devices you already own — and keeps everything encrypted at rest on hardware you control. No vendor cloud, no subscription, no telemetry.
HealthLog is a personal health record-keeping and wellness tool. It is not a medical device and does not diagnose, treat, cure, or prevent any disease — it helps you log, visualise, and export your own data, and leaves clinical judgement to a clinician.
Try the live demo to see a working install — sign in with demo / demo123demo123.
Highlights
- Every vital on one timeline. Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking — charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024), overridable with the targets your clinician set.
- Clinical signals, opt-in and additive. Validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7, Sleep Condition Indicator — official questionnaire wording, calm crisis-resource handling), grip strength, a 0–10 pain score, waist and body measurements, and a longevity lab panel — each added through one signal registry, so a new signal wires into charts, correlations, the coach, the FHIR map, and the API at once.
- Records, structured. Allergies and family history as encrypted structured records that flow into the doctor report, plus lab values you key in or scan from a printed report — OCR extracts the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. (Immunization records are deliberately out of scope.)
- A document vault for the paper trail. An opt-in module stores every letter, report, and scan encrypted at rest — PDFs and photos preview inline, Office and text files stay download-only, and nothing user-uploaded can ever execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, undo-able delete with a 30-day grace, and per-file/per-account limits the operator tunes.
- Context that explains the numbers. An opt-in environmental module folds weather, daylight, and temperature in as influence signals (winter darkness against mood, heat against blood pressure) using a home location you set, so a pattern has somewhere to point.
- Your devices, one server. Withings, WHOOP, Oura and Polar sync over OAuth2, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health
export.zipfolds your full history in, and the native iOS app streams HealthKit live. A per-metric source priority decides which reading is canonical when wearables overlap. - Medication tracking that tells the truth. Flexible schedules (weekly injections, weekday-only, intervals, PRN, cyclic), a configurable intake window per dose, and a traceable dose history where every slot reads taken, late, skipped, or missed — the same ledger the compliance rate is computed from, so the percentage can never disagree with the timeline.
- AI insights you own. Daily briefing, health scores, correlations, and a conversational coach grounded in your own measurements and interpreted against cited guideline reference bands — where a value sits and what that means in plain words, never a diagnosis. The coach proposes concrete if-then plans you accept or decline, and suppresses proactive check-ins when there's nothing genuine to say. Bring your own OpenAI or Anthropic key, or point at a local endpoint (Ollama, LM Studio, vLLM) so nothing leaves your network.
- Clinician-ready output. A doctor-report PDF generated client-side, a read-only HL7 FHIR R4 API, and scoped, time-limited share links you revoke after the visit.
- Private by construction. AES-256-GCM encryption at rest with zero-downtime key rotation, passkey login, server-side sessions, strict CSP — and no third-party tracking anywhere.
- Built to be lived in. Installable PWA with offline support, medication reminders over APNs, Telegram, ntfy, and Web Push, a sub-second dashboard on years of imported history — in English, German, Spanish, French, Italian, Polish, and Korean end to end.
The full feature tour, integration guides, and API reference live at docs.healthlog.dev.
Quick start (Docker Compose)
git clone https://github.com/MBombeck/HealthLog.git && cd HealthLog
cp .env.example .env
# Generate the three secrets (each a 64-char hex string = 32 bytes):
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env
# LAN / plain-HTTP host (NAS, homelab, Tailscale) — required, or login fails:
echo "SESSION_COOKIE_SECURE=false" >> .env
docker compose up -d
Open http://localhost:3000 (or http://<host>:3000) — the first registered user becomes admin. The compose file pulls a pre-built multi-arch image (amd64 + arm64) from GHCR; no build step required.
Verify the running version:
curl -s http://localhost:3000/api/version # returns version + buildSha + builtAt
SESSION_COOKIE_SECURE=false is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the Secure flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or true) only when a TLS reverse proxy serves HTTPS — and then set NEXT_PUBLIC_APP_URL and APP_URL to the public URL first (see the self-hosting guide).
Persistent state lives in exactly one named volume (pgdata, the Postgres data dir); the app container is stateless, so backing up the database backs up everything.
NAS and homelab users have ready-made paths: Unraid Community Applications and Portainer app templates.
Self-hosting
One container hosts both the web server and the job worker by default; split them via HEALTHLOG_PROCESS_TYPE=web|worker for horizontal scale. Encrypted daily backups to any S3-compatible bucket are opt-in via the admin panel, new users join by invite link or QR code, and the stack works out of the box behind Caddy, Traefik, Nginx, or Coolify.
HealthLog ships releases roughly weekly. Pin a tag, back up before upgrades, and skim the CHANGELOG before pulling latest. The operator manual — reverse proxy, migrations, encryption-key rotation, backup and restore — lives at docs.healthlog.dev.
Native iOS app
A SwiftUI companion in public beta via TestFlight, built on the same API as the web client: live HealthKit two-way sync, medication reminders with action buttons that work without opening the app, and an on-device coach on Apple-Intelligence-capable iPhones. Code lives in MBombeck/healthlog-iOS.
Tech stack
Next.js 16 (App Router, React Server Components), TypeScript strict, PostgreSQL 16 with Prisma, pg-boss for jobs, Tailwind CSS 4 + shadcn/ui, Recharts, WebAuthn passkeys, Vitest + Playwright, multi-stage Alpine Docker image. The OpenAPI 3.1 contract for native clients is generated from the Zod schemas at docs/api/openapi.yaml.
Documentation
| User and operator docs | docs.healthlog.dev |
| Release history | CHANGELOG.md |
| API contract | docs/api/openapi.yaml |
| Security policy | SECURITY.md |
| Contributing | CONTRIBUTING.md |
Status
Actively developed — new releases roughly weekly, issue reports and PRs welcome. Behaviour and schema can change between versions; migrations are forward-only.
License
HealthLog is licensed under the PolyForm Noncommercial License 1.0.0: free to use, self-host, and modify for noncommercial purposes. Commercial use requires a separate agreement — open an issue or reach out via healthlog.dev. Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that license.
healthlog.dev · Live Demo · Docs · iOS TestFlight · Buy Me a Coffee