From e8ef590af7058f531885ec19011d9cdd790706ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 6 Nov 2026 17:53:50 +0100 Subject: [PATCH] docs: describe the cycle calendar subscription A how-to for subscribing from Google, Apple and Outlook and what the feed shows; a section on the reproductive-health page saying that once it is on, the dates also sit with the calendar provider and turning it off does not reach those copies; a reverse-proxy note with Caddy and Nginx examples for keeping path credentials out of access logs; and API_TOKEN_HMAC_KEY rotation named as ending every feed link. --- docs/README.md | 2 + docs/self-hosting/getting-started.md | 14 ++--- docs/self-hosting/reproductive-health-data.md | 21 ++++++++ docs/self-hosting/reverse-proxy.md | 43 +++++++++++++++ docs/using/cycle-calendar-feed.md | 53 +++++++++++++++++++ 5 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 docs/using/cycle-calendar-feed.md diff --git a/docs/README.md b/docs/README.md index b500f6784..58ccae26c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,6 +45,8 @@ notes, and machine-readable specs that ship alongside the source: code. [`using/search.md`](./using/search.md) covers the command palette's record search: opening it, what it finds, how matching works, and its limits on very large and shared records. + [`using/cycle-calendar-feed.md`](./using/cycle-calendar-feed.md) covers + subscribing to the cycle forecast from Google, Apple or Outlook. - [`migration/`](./migration/) — migration notes. Currently only v1.3 to v1.4; later releases are covered by `CHANGELOG.md` and . diff --git a/docs/self-hosting/getting-started.md b/docs/self-hosting/getting-started.md index fd7570fd2..039ecfd43 100644 --- a/docs/self-hosting/getting-started.md +++ b/docs/self-hosting/getting-started.md @@ -72,11 +72,11 @@ echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env ``` -| Variable | Purpose | Rotation cost | -| -------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| `POSTGRES_PASSWORD` | Bundled Postgres password — used by both the `db` service and `DATABASE_URL` | Database restart + connection-string update | -| `ENCRYPTION_KEY` | AES-256-GCM key for at-rest secrets (Withings tokens, AI provider keys, VAPID secrets) | Coordinated; see `docs/ops/encryption-key-rotation.md` | -| `API_TOKEN_HMAC_KEY` | HMAC-SHA256 key for hashing Bearer API tokens and web session cookies before storage | Invalidates every issued `hlk_*` token and signs out every browser session | +| Variable | Purpose | Rotation cost | +| -------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `POSTGRES_PASSWORD` | Bundled Postgres password — used by both the `db` service and `DATABASE_URL` | Database restart + connection-string update | +| `ENCRYPTION_KEY` | AES-256-GCM key for at-rest secrets (Withings tokens, AI provider keys, VAPID secrets) | Coordinated; see `docs/ops/encryption-key-rotation.md` | +| `API_TOKEN_HMAC_KEY` | HMAC-SHA256 key for hashing Bearer API tokens and web session cookies before storage | Invalidates every issued `hlk_*` token, share link and cycle calendar-feed link, and signs out every browser session | You do **not** edit `DATABASE_URL` on the bundled `docker-compose.yml` stack: compose constructs the connection string inside the `app` @@ -289,8 +289,8 @@ this server needs: retired id for as long as a stored or off-host backup copy still needs it; - `BACKUP_ENCRYPTION_KEY` and any `BACKUP_ENCRYPTION_PREVIOUS_KEYS`, when off-host copies are set up; -- `API_TOKEN_HMAC_KEY`, without which every session, token and share link ends - (no data is lost). +- `API_TOKEN_HMAC_KEY`, without which every session, token, share link and + calendar-feed link ends (no data is lost). Each key is shown by id and a fingerprint (the first 12 hex characters of SHA-256 over the key), never the key itself, and **Check my copy** compares a diff --git a/docs/self-hosting/reproductive-health-data.md b/docs/self-hosting/reproductive-health-data.md index 1fcaec46a..1639f9890 100644 --- a/docs/self-hosting/reproductive-health-data.md +++ b/docs/self-hosting/reproductive-health-data.md @@ -121,6 +121,27 @@ the grace window to 0 or wait it out, remove the old volume, and let the dumps age out or delete them ([backup-restore.md](../ops/backup-restore.md)). +## Calendar subscription + +The cycle settings can publish your forecast as a calendar feed that Google +Calendar, Apple Calendar or Outlook subscribes to. It is off by default, and +turning it on changes the arrangement this page describes: from then on the +feed's dates also sit with that calendar provider, on its servers, under its +policies. That is exactly the cloud vendor self-hosting otherwise keeps out of +the picture. + +What the feed carries is deliberately small. Each event is an all-day date +range and a title, nothing else: no symptoms, notes, flow, temperatures, test +results, contraception or intercourse fields. By default the title is a dot +and the calendar is named "HealthLog", so a glance at a shared screen says +nothing; past cycles are left out unless you ask for them, and the fertile +window is only offered when your goal is trying to conceive. + +Turning the subscription off, or rotating its link, stops the provider's next +poll. It does not reach the copies the provider already fetched: remove the +subscribed calendar there as well. The link itself is a credential; see +[keeping it out of access logs](./reverse-proxy.md#credentials-in-the-url-path-keep-them-out-of-access-logs). + ## Practical hardening If reproductive-health privacy is the reason you self-host, the diff --git a/docs/self-hosting/reverse-proxy.md b/docs/self-hosting/reverse-proxy.md index 3cceebaac..fefca74b2 100644 --- a/docs/self-hosting/reverse-proxy.md +++ b/docs/self-hosting/reverse-proxy.md @@ -336,6 +336,49 @@ A `TRUST_PROXY_HOPS` value that is not a whole number resolves no client address at all (requests keep working) and is named in the `[proxy-trust]` boot line. +## Credentials in the URL path: keep them out of access logs + +A few HealthLog URLs carry their credential in the path, because the caller +cannot send a header: the cycle calendar feed (`/api/cycle/feed/hlf_…`, +polled by Google, Apple or Outlook), the clinician share view (`/c/hls_…`) +and the provider webhooks. HealthLog scrubs these segments from its own logs +and error reports. It cannot reach your proxy's access log, which records the +full path of every request by default, so anyone who can read that log can +read a live feed link. + +Either drop those requests from the access log or rewrite the path before it +is written. For Caddy, skip logging the token-bearing paths: + +```caddyfile +your-instance.example.com { + @credential_paths path /api/cycle/feed/* /c/* + log_skip @credential_paths + reverse_proxy app:3000 +} +``` + +For Nginx, log a redacted path instead of `$request`: + +```nginx +map $request_uri $redacted_uri { + ~^/api/cycle/feed/ /api/cycle/feed/[REDACTED]; + ~^/c/ /c/[REDACTED]; + default $request_uri; +} +log_format redacted '$remote_addr - [$time_local] "$request_method $redacted_uri" ' + '$status $body_bytes_sent "$http_user_agent"'; +access_log /var/log/nginx/access.log redacted; +``` + +Traefik's access log has no per-path filter; turn `accessLog` off, or keep it +on a host only you can read. If a feed link has been in a log you do not +control, rotate it in the app (Cycle, Settings, Calendar subscription, Rotate +link): the old one stops working on the provider's next poll. + +The feed answers with `Cache-Control: private, no-cache` and an `ETag`, so a +caching proxy in front must pass `If-None-Match` through rather than cache the +body for everyone. + ## Running over plain HTTP on a LAN (no reverse proxy) The session cookie carries the `Secure` flag by default under diff --git a/docs/using/cycle-calendar-feed.md b/docs/using/cycle-calendar-feed.md new file mode 100644 index 000000000..e8a8335d7 --- /dev/null +++ b/docs/using/cycle-calendar-feed.md @@ -0,0 +1,53 @@ +# Subscribing to your cycle forecast + +HealthLog can publish your cycle forecast as a calendar subscription, so the +next few periods show up in the calendar app you already use. The feed is +computed fresh every time your calendar app asks for it; HealthLog stays the +only place the data is kept. + +## Turning it on + +Open **Cycle**, then **Settings**, and find **Calendar subscription**. Before +the button, the card says what it means: your calendar provider stores the +dates on its servers, and turning the subscription off later does not remove +copies it already holds. HealthLog asks you to confirm it is you (a password or +passkey) and then shows the link once. Copy it, or use **Open in calendar app**. + +Only the record's owner, or someone who manages it with a MANAGE grant that +covers cycle data, can turn it on. It is managed on the web; the iOS app shows +where to find it. + +## Adding it to your calendar + +- **Google Calendar:** Other calendars, From URL, paste the link. Google + refreshes subscriptions on its own schedule, roughly every 12 to 24 hours. +- **Apple Calendar:** on a Mac, File, New Calendar Subscription. On iPhone, + Settings, Calendar, Accounts, Add Account, Other, Add Subscribed Calendar. + Apple lets you choose how often it refreshes. +- **Outlook:** Add calendar, Subscribe from web, paste the link. Outlook + refreshes every few hours. + +## What the feed shows + +- The next period and, once three cycles are logged, a few after it (three + by default, up to six). Each event covers the whole window the period may + fall in, and the window widens the further out it is. The list stops itself + where a date would be more guess than estimate (beyond about ±10 days, or + half a cycle). +- Optionally, your past cycles (up to twelve). +- Optionally, when your goal is trying to conceive, the estimated fertile + window of the next cycle. It is an estimate, not a contraceptive method. + +Titles are discreet by default: each event is a dot and the calendar is called +"HealthLog". Descriptive titles name the period instead. Events never carry +symptoms, notes or any other detail of your log. + +When a period is later than its window, the feed shows no forecast until you +log it, instead of guessing a new date. With predictions switched off, only +past cycles appear (if you chose to include them). + +## Rotating and turning off + +**Rotate link** replaces the link; calendars using the old one stop updating. +**Turn off** ends the link. In both cases, remove the old subscribed calendar +in your calendar app too: what it already fetched stays there until you do.