diff --git a/.env.production.example b/.env.production.example index 258db482e..3b94d3963 100644 --- a/.env.production.example +++ b/.env.production.example @@ -74,13 +74,35 @@ API_TOKEN_HMAC_KEY="" # OURA_REDIRECT_URI="" +# ----------------------------------------------------------------------------- +# Web Push -- VAPID keys (optional; the admin panel is the easier path) +# ----------------------------------------------------------------------------- +# Web Push needs NO Apple account and no paid service -- just a VAPID keypair. +# The EASIEST path is the admin panel: /admin -> Web Push VAPID -> "Generate +# keys" mints and stores the pair for you (private key encrypted at rest). Use +# these env vars only if you prefer env-config over the DB. The loader reads +# DB first, then env (src/lib/notifications/vapid-config.ts). NOTE: these are +# NOT in docker-compose.yml's `environment:` whitelist -- if you go the env +# route under compose, add them there too or the values never reach the +# container (see docs/self-hosting/getting-started.md Troubleshooting). +# VAPID_PUBLIC_KEY="" +# VAPID_PRIVATE_KEY="" +# VAPID_SUBJECT="mailto:you@example.com" + + # ----------------------------------------------------------------------------- # APNs -- iOS push (optional, all-or-none) # ----------------------------------------------------------------------------- +# Only the native iOS app uses APNs, and it needs a paid Apple Developer +# Program membership. Web Push (above) needs NO Apple account -- it covers +# every browser and the installed PWA on iPhone. Leave this whole block unset +# unless you ship the native app. Key-source precedence when more than one is +# set: APNS_KEY_B64 > APNS_KEY > APNS_KEY_FILE. # APNS_KEY_ID="" # APNS_TEAM_ID="" # APNS_BUNDLE_ID="" -# APNS_KEY="" # OR APNS_KEY_FILE -- either one satisfies the manifest +# APNS_KEY_B64="" # PEM body base64-encoded -- recommended (escapes newlines) +# APNS_KEY="" # OR APNS_KEY (raw PEM) OR APNS_KEY_FILE -- one satisfies the manifest # APNS_KEY_FILE="" diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index ca8abe9a6..5bee1db7d 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -175,3 +175,51 @@ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build - **OAuth callbacks loop back to localhost.** Confirm `APP_URL` and `NEXT_PUBLIC_APP_URL` both point at the public hostname, not at `localhost`, and restart `app` after the change. +- **An env var in `.env` seems ignored / a feature stays "off".** The + compose `environment:` block is a **whitelist** — a variable set in + `.env` only reaches the container if `docker-compose.yml` lists it + under `environment:`. Vars not listed never propagate, even with + `${VAR}` substitution at compose-up. If a setting looks configured but + is silently dead, check the var is in that block (add it if you run a + custom compose); the supported alternative for most secrets (VAPID, + GitHub token, GlitchTip DSN) is the **admin panel**, which stores them + in the database and sidesteps the whitelist entirely. +- **A new image doesn't pick up / `:latest` looks stale.** `docker-compose.yml` + sets `pull_policy: always`, which is load-bearing: without it Docker + re-uses the cached `:latest` digest and silently skips the registry + round-trip on `compose up`. If you pin a custom compose, keep + `pull_policy: always` on the `app` service, and verify the running + build with `GET /api/version` (it returns `version` + `buildSha`). + +## Backup and restore + +Your database is the only stateful piece — back it up, and back up the +key that decrypts it. + +**Local snapshot (the common single-box case).** Dump the `db` service +with `pg_dump`: + +```bash +# The bundled db service uses user `healthlog` and database `healthlog`. +docker compose exec -T db pg_dump -U healthlog healthlog \ + | gzip > healthlog-$(date +%F).sql.gz +``` + +Restore into a fresh database the same way: + +```bash +gunzip -c healthlog-2026-01-01.sql.gz \ + | docker compose exec -T db psql -U healthlog healthlog +``` + +> **Back up your `ENCRYPTION_KEYS` / `ENCRYPTION_KEY` alongside the dump.** +> HealthLog encrypts sensitive columns at rest (AES-256-GCM). A database +> dump **without the encryption key is unrecoverable** — the encrypted +> data cannot be read back. Store the key (and any retired keys still in +> the rotation map) somewhere separate from the dump but equally durable. +> Losing the key means losing the encrypted data, full stop. + +For off-host encrypted backups to S3/R2/B2 see +[`docs/ops/backup-restore.md`](../ops/backup-restore.md); for rotating the +encryption key safely (keep the old key until zero rows remain on it) see +[`docs/ops/encryption-key-rotation.md`](../ops/encryption-key-rotation.md). diff --git a/docs/self-hosting/notifications.md b/docs/self-hosting/notifications.md index affe53abe..7abf16df0 100644 --- a/docs/self-hosting/notifications.md +++ b/docs/self-hosting/notifications.md @@ -34,35 +34,46 @@ mobile — even when the HealthLog tab is closed, as long as the browser or installed PWA is alive. It is the broadest-reach channel and the one to set up first. -### 1. Generate a VAPID keypair +### 1. Generate and store a VAPID keypair VAPID is the signing scheme that lets a push service trust your server. -Generate a keypair once with the bundled `web-push` CLI: +You also choose a **subject** — a `mailto:` address the push service can +contact about your sender, e.g. `mailto:you@example.com`. + +The server loads VAPID config from the database first and falls back to +environment variables (`src/lib/notifications/vapid-config.ts`). The admin +panel is the easiest path because it survives without touching `.env`. + +**Admin panel — Generate keys (easiest).** Sign in as the admin user, +open `/admin`, and find the **Web Push VAPID** card +(`src/components/admin/web-push-vapid-section.tsx`). Click **Generate +keys**: the server mints a fresh keypair, stores the private key encrypted +at rest with your `ENCRYPTION_KEY`, seeds a placeholder subject, and fills +in the public key. Edit the subject to your real `mailto:` address and +save. The card shows a "configured" badge once all three fields are set — +no shell, no copy-paste. + +> If a keypair already exists, **Generate keys** asks you to confirm before +> replacing it. Regenerating invalidates every existing browser +> subscription, so each device has to re-subscribe afterwards. Only +> regenerate when you mean to. + +**Admin panel — paste an existing pair.** If you already have a keypair +(e.g. copied from another deployment), paste the public key, the private +key, and the subject into the same card and save instead of generating. + +**CLI — generate a pair yourself.** You can also mint one with the bundled +`web-push` CLI and paste the result into the card or your `.env`: ```bash npx web-push generate-vapid-keys ``` -That prints a **public key** and a **private key** (both Base64URL). You -also choose a **subject** — a `mailto:` address the push service can -contact about your sender, e.g. `mailto:you@example.com`. +That prints a **public key** and a **private key** (both Base64URL). -### 2. Store the keys +### 2. Store the keys via environment variables (alternative) -The server loads VAPID config from the database first and falls back to -environment variables (`src/lib/notifications/vapid-config.ts`). Either -path works; the admin panel is the easier one because it survives without -touching `.env`. - -**Admin panel (preferred).** Sign in as the admin user, open -`/admin`, and find the **Web Push VAPID** card -(`src/components/admin/web-push-vapid-section.tsx`). Paste the public key, -the private key, and the subject, then save. The private key is encrypted -at rest with your `ENCRYPTION_KEY`, exactly like the other secrets in the -admin panel. The card shows a "configured" badge once all three fields -are set. - -**Environment variables (alternative).** If you would rather keep +If you would rather keep secrets out of the database, set them in `.env` / the compose `environment:` block instead: @@ -179,6 +190,14 @@ Two gotchas worth knowing before you blame a stale token: `BadEnvironmentKeyInToken`. Confirm the key shows **Sandbox & Production**; a wrongly scoped key cannot be reconfigured — re-issue it. +> **`clientManaged` is iOS-app-only.** The native iOS app can opt to run +> its own local medication reminders and ask the server to stop sending +> the duplicate `MEDICATION_REMINDER` push (the `clientManaged` flag on +> `PATCH /api/auth/me/notification-prefs`). This is a native-app contract: +> a web/PWA-only self-hoster never needs it and there is no toggle for it +> in the web UI. If you only run the PWA, ignore it entirely — your +> medication reminders come from the server cron over the channels above. + ## Which channel should I pick? | Your setup | Recommended channel |