# ============================================================================= # HealthLog -- production .env example # ============================================================================= # Mirrors `scripts/env-manifest.json`. The `pnpm check-env --file # .env.production.example` CI step (see `.github/workflows/env-check.yml`) # enforces every variable declared `required: true` in the manifest has a # non-empty placeholder here, so a contributor who introduces a new # required var without updating this file fails the workflow. # # Placeholders are syntactic only -- replace with real secrets before # deploying. The check-env presence rule treats any non-empty, # non-whitespace string as "set", so the `` style below # satisfies the manifest without leaking a usable credential. # # Generate the two crypto secrets locally with: # $ openssl rand -hex 32 # ============================================================================= # ----------------------------------------------------------------------------- # Core -- required for boot # ----------------------------------------------------------------------------- DATABASE_URL="postgresql://healthlog:@db:5432/healthlog?schema=public" POSTGRES_PASSWORD="" NEXT_PUBLIC_APP_URL="https://healthlog.example.com" APP_URL="https://healthlog.example.com" ENCRYPTION_KEY="" API_TOKEN_HMAC_KEY="" # ----------------------------------------------------------------------------- # Withings OAuth -- optional integration # ----------------------------------------------------------------------------- # WITHINGS_CLIENT_ID="" # WITHINGS_CLIENT_SECRET="" # WITHINGS_WEBHOOK_SECRET="" # ----------------------------------------------------------------------------- # WHOOP -- optional integration # ----------------------------------------------------------------------------- # The WHOOP app client id/secret are per-user BYO-keys: each user registers # their own WHOOP developer app and pastes the client id/secret into Settings # (stored encrypted in the DB). There is therefore no WHOOP_CLIENT_ID/SECRET. # # WHOOP_WEBHOOK_SECRET is the instance-level secret carried as the trailing # path segment of the webhook URL AND used to verify the WHOOP HMAC body # signature. WHOOP_REDIRECT_URI overrides the derived # `${NEXT_PUBLIC_APP_URL}/api/whoop/callback` when the public URL differs. # WHOOP_WEBHOOK_SECRET="" # WHOOP_REDIRECT_URI="" # ----------------------------------------------------------------------------- # Polar AccessLink -- optional integration (full OAuth, shared app from env) # ----------------------------------------------------------------------------- # Register an app at https://admin.polaraccesslink.com and set its redirect URI # to `${NEXT_PUBLIC_APP_URL}/api/polar/callback`. Both vars together enable the # Polar connect button; set neither to leave the integration disabled. Polar # access tokens do not expire and carry no refresh token. POLAR_REDIRECT_URI # overrides the derived callback when the public URL differs. # POLAR_CLIENT_ID="" # POLAR_CLIENT_SECRET="" # POLAR_REDIRECT_URI="" # ----------------------------------------------------------------------------- # Oura Cloud v2 -- optional integration (full OAuth, shared app from env) # ----------------------------------------------------------------------------- # Register an app at https://cloud.ouraring.com/oauth/applications and set its # redirect URI to `${NEXT_PUBLIC_APP_URL}/api/oura/callback`. Both vars together # enable the Oura connect button. Oura uses refresh tokens; the sync refreshes # reactively on a 401. OURA_REDIRECT_URI overrides the derived callback. # OURA_CLIENT_ID="" # OURA_CLIENT_SECRET="" # 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_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="" # APNS_CRITICAL_ENTITLEMENT="" # "true" only if your build has Apple's Critical Alerts entitlement; else urgent = time-sensitive # ----------------------------------------------------------------------------- # SMTP -- email notification channel (optional, all-or-none) # ----------------------------------------------------------------------------- # Operator SMTP transport for the email channel. SMTP_HOST + SMTP_PORT + # SMTP_FROM together enable it; SMTP_USER/SMTP_PASS are optional (omit for an # unauthenticated relay). SMTP_SECURE=true uses implicit TLS (port 465); # unset/false uses STARTTLS (port 587). Per-user recipient address is set in # Settings -> Notifications, not here. Missing the core trio disables the # channel silently. Listed in docker-compose.yml's environment whitelist so # these reach the container. # SMTP_HOST="" # SMTP_PORT="587" # SMTP_FROM="HealthLog " # SMTP_USER="" # SMTP_PASS="" # SMTP_SECURE="false" # ----------------------------------------------------------------------------- # Deploy webhook -- Coolify -> HealthLog auto-deploy feedback # ----------------------------------------------------------------------------- # DEPLOY_WEBHOOK_SECRET="" # Optional: deployment-dashboard URL (e.g. the Coolify UI) linked in # deploy-failure notifications. When unset the notification omits the link. # DEPLOY_LOGS_URL="" # Optional replay hardening: require a fresh X-Deploy-Webhook-Timestamp # header (unix seconds / ms or ISO-8601, +/- 5 minutes) on every webhook # call. Leave unset for Coolify's stock sender (it attaches no # timestamp); see docs/ops/deploy.md. # DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP="false" # ----------------------------------------------------------------------------- # TLS leaf pin monitor -- alarm when the pinned leaf certificate rotates # ----------------------------------------------------------------------------- # The native client SPKI-pins the served TLS leaf. Set this to the # known-good leaf pin(s) so the monitor can alarm on a rotation. The value # is base64(sha256(DER subjectPublicKeyInfo)); comma-separate to hold both # the current and the next pin during a dual-pin renewal window. See # docs/ops/tls-cert-pin.md for extraction + the re-pin runbook. # TLS_LEAF_SPKI_PINS="" # ----------------------------------------------------------------------------- # Off-host backups -- all-or-none # ----------------------------------------------------------------------------- # BACKUP_S3_ENDPOINT="" # BACKUP_S3_BUCKET="" # BACKUP_S3_ACCESS_KEY="" # BACKUP_S3_SECRET_KEY="" # BACKUP_ENCRYPTION_KEY="" # ----------------------------------------------------------------------------- # FHIR health-record export -- optional # ----------------------------------------------------------------------------- # Per-export ceiling on the number of MedicationAdministration rows in the # FHIR Bundle. The report window already scopes the set; this is a coarse # safety ceiling on the serialised Bundle size. Bounded 1..50000; any unset # or out-of-range value falls back to the default of 5000. Raise it only for # an unusual multi-year, many-medication workload. # FHIR_MAX_MEDICATION_ADMINISTRATIONS="5000" # ----------------------------------------------------------------------------- # Dashboard snapshot -- BUILD-TIME toggle (default ON) # ----------------------------------------------------------------------------- # The dashboard hydrates every above-the-fold tile from the single # `GET /api/dashboard/snapshot` cell so the whole strip shares one # completion moment. This is the default and the desired behaviour. # # `NEXT_PUBLIC_*` vars are inlined into the client bundle at BUILD time, # not read at runtime -- so this has NO effect on the prebuilt GHCR # image and is intentionally absent from the `docker-compose.yml` # runtime `environment:` whitelist. To fall back to the legacy # four-cell path you must rebuild the image with the var set to "false": # # $ NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false pnpm build # or a Dockerfile ARG # # NEXT_PUBLIC_DASHBOARD_SNAPSHOT="false"