docs(self-hosting): VAPID generate button, env names, backup and whitelist notes

Document the new Generate keys button in notifications.md as the easiest
Web Push setup path, demoting the CLI to an alternative. Note that
clientManaged medication-reminder suppression is an iOS-app-only contract
so a PWA-only operator does not go hunting for a web toggle.

Add the VAPID env-var names and APNS_KEY_B64 to .env.production.example
(the loader and APNs sender read them, but the example omitted them),
both as commented placeholders, with a note that Web Push needs no Apple
account and that these vars are not in the compose whitelist.

Add a Backup and restore section to getting-started.md: a local pg_dump
one-liner and the load-bearing warning that the ENCRYPTION_KEYS /
ENCRYPTION_KEY must be backed up alongside the dump or the encrypted data
is unrecoverable, cross-linking the off-host backup and key-rotation
runbooks. Add troubleshooting entries for the compose env-var whitelist
and pull_policy: always.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 00:58:39 +02:00
1 parent d0ba4b061b
commit 266109f501
3 files changed
+110 -21

No files matched your search

+23 -1
View File
@@ -74,13 +74,35 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
# 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=""
+48
View File
@@ -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).
+39 -20
View File
@@ -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 |