Files
HealthLog/docs/self-hosting/scaling.md
T
Marc-André Bombeck f5962497d6 v1.4.34.4 — audit-driven hotfix bundle (security, UX, code-quality, docs)
Consolidated hotfix landing the findings from a six-axis audit
covering mobile security, UX + accessibility, code quality + docs,
README + discoverability, web performance, and mobile-deep responsive
behaviour. None of the changes break the public API or the existing
data shape; every fix is additive and SAFE under the v1.4.34.x web
freeze. Full audit reports live in `.planning/round-audit-*.md`.

Security
  - Password rotation now wipes API tokens + refresh tokens in the
    same `$transaction` as session revocation. A stolen iOS Bearer no
    longer survives a user-initiated password change or an admin reset.
  - `/.well-known/` proxy bypass tightened to an explicit-allowlist
    `Set`. Stale `/api/auth/codex/callback` PUBLIC_PATHS entry dropped.
  - HSTS gains `preload`. Withings host removed from the global CSP
    and gated to the integration's own routes.
  - `getClientIp` warns once per process on `TRUST_PROXY_HOPS`/XFF
    mismatch + exposes a tagged helper for tighter universal buckets.
  - Service worker rejects off-origin notification-click URLs. Offline
    fallback body switched to a locale-neutral icon. Manifest gains
    `scope`, `id`, `display_override`.

UX + accessibility
  - i18n leaks closed on Dialog/Sheet close-X, ChartSkeleton's loading
    announcement, and the notifications-section breadcrumb.
  - Focus rings on bottom-nav, the More overflow button, and the
    mobile top-bar user menu.
  - Tap-target floor: Input / NativeSelect / Select / DateTimeInput
    switch to `h-11 sm:h-10`.
  - Admin chip-strip auto-scroll-into-view (matches v1.4.33 settings).
  - List-page subtitles visible on mobile.
  - Insights empty-state CTA vocabulary normalised so the link opens
    the right measurement form (was silently failing for
    BODY_TEMPERATURE / BLOOD_GLUCOSE / PULSE).

Mobile-deep
  - `viewport.themeColor` reacts to light/dark mode.
  - TopBar + Coach FAB reserve safe-area-inset-top / -bottom for
    notched iPhones.
  - Sonner toaster switches to `theme="system"`.

Code quality
  - `SESSION_SECRET` dropped — the env var was documented as required
    but never read by code. Three secrets, not four. Self-hosters
    upgrading can delete the existing line.
  - `toJson<T>()` helper consolidates seven Prisma JSON-cast sites.
  - `db-compat.ts` gains a header docstring naming it the
    schema-bootstrap path.
  - Passkey response types adopted from `@simplewebauthn/server`;
    `any` + eslint-disable pragmas drop out.
  - Zero-TODO CI gate added at `.github/workflows/no-todo-markers.yml`.

Documentation + discoverability
  - README rework: How-it-compares matrix, Apple Health + AI Coach
    promoted to standalone Key-Features bullets, status block, latest
    release + GHCR badges, OpenAPI pointer, three-secret Quick Start.
  - openGraph + twitter cards now carry `images` and a
    `summary_large_image` Twitter card.
  - GitHub repo metadata: description tightened from 290 to 156
    chars + keyword-front, `homepageUrl` flipped from a personal
    tenant to the demo site, six generic / PII-adjacent topics
    swapped for six high-intent ones.
  - Five new user-facing docs: `docs/self-hosting/getting-started.md`,
    `docs/self-hosting/reverse-proxy.md`,
    `docs/integrations/apple-health.md`,
    `docs/integrations/withings.md`,
    `docs/integrations/ai-providers.md`.
  - `docs/README.md` clarifies the docs tree as internal playbooks
    vs the user-facing `docs.healthlog.dev` site.
2026-05-17 00:39:58 +02:00

53 lines
2.4 KiB
Markdown

# Scaling: web/worker container split (v1.4 G3)
The default `docker compose up` launches a single container that runs
both the Next.js HTTP server AND the pg-boss worker. That works fine
for a personal deployment but is hard to scale horizontally because
every replica also runs the full reminder/insight cron schedule.
v1.4 introduces an environment-variable gate so the same image can be
deployed in three modes:
| `HEALTHLOG_PROCESS_TYPE` | Web | Worker | Use when |
| ------------------------ | --- | ------ | ------------------------- |
| `all` (default) | ✅ | ✅ | single-host self-hosting |
| `web` | ✅ | ❌ | scaling HTTP horizontally |
| `worker` | ❌ | ✅ | dedicated job worker |
## How to split
1. Edit `docker-compose.yml`: set `HEALTHLOG_PROCESS_TYPE=web` on the
`app` service and uncomment the `app-worker` service block at the
bottom (it ships with `HEALTHLOG_PROCESS_TYPE=worker` baked in).
2. Make sure both containers see the same `DATABASE_URL`,
`ENCRYPTION_KEY` (or `ENCRYPTION_KEYS`), and `API_TOKEN_HMAC_KEY`.
3. `docker compose up -d --build`.
Both containers connect to the same Postgres. pg-boss claims jobs
atomically via row-level locking, so it is safe to run multiple worker
replicas — they will share the load.
## Healthchecks
- `app` (web) healthcheck: `wget /api/version` every 30s.
- `app-worker` does not expose a port; its liveness is reported into
the `worker_status` table on every reminder pass and surfaced through
`/api/admin/worker-status`.
The web container does NOT depend on the worker, and vice versa, so
neither container's startup can deadlock the other. The shared
dependency is Postgres; both wait on `db: condition: service_healthy`.
## Caveats
- Background tasks that touch user-scoped Wide Events (e.g. reminder
notifications) emit telemetry from the worker; configure `LOKI_*`
env vars on the worker if you want them.
- The off-host backup job runs in the worker only. The admin
`POST /api/admin/backup/test` endpoint runs in the web container and
exercises the same S3 credentials — set `BACKUP_*` env vars on BOTH.
- The startup gate (`assertSubsystemEnabled`) refuses to boot the
reminder worker when `HEALTHLOG_PROCESS_TYPE=web`, so an accidental
cross-mode invocation aborts immediately instead of silently doubling
cron load.