# ============================================================================= # 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 -- optional integration # ----------------------------------------------------------------------------- # The Withings OAuth client id/secret are per-user: each user registers their # own Withings developer app and pastes the pair into Settings (stored # encrypted in the DB). There is therefore no WITHINGS_CLIENT_ID/SECRET. # WITHINGS_WEBHOOK_SECRET="" # Overrides the derived "/api/withings/callback". Set it # when the callback registered with Withings differs from the app's own # public URL -- e.g. a second hostname in front of the same instance. # WITHINGS_REDIRECT_URI="" # ----------------------------------------------------------------------------- # Google Health Connect -- optional integration (per-user BYO-keys) # ----------------------------------------------------------------------------- # Like Fitbit, the client id/secret are per-user and live encrypted in the DB. # This only overrides the derived # "/api/google-health/callback"; it must stay same-origin # with NEXT_PUBLIC_APP_URL or the handshake is refused. # GOOGLE_HEALTH_REDIRECT_URI="" # ----------------------------------------------------------------------------- # Telegram -- optional notification channel # ----------------------------------------------------------------------------- # Shared secret for the Telegram bot webhook. It is both the path segment the # bot posts to and the value compared against the update header, so set it # before installing the webhook from Settings -> Notifications. Without it the # settings page cannot register the webhook and inbound updates are refused. # TELEGRAM_WEBHOOK_SECRET="" # ----------------------------------------------------------------------------- # Nightscout -- optional integration # ----------------------------------------------------------------------------- # Nightscout instances on your own network that the per-user connection form # should accept, comma-separated (e.g. "http://nightscout.lan:1337"). The # validator refuses private and internal hosts by default; this is the opt-in # for a self-hosted Nightscout that is not reachable from the public internet. # NIGHTSCOUT_PRIVATE_ORIGINS="" # ----------------------------------------------------------------------------- # 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="" # ----------------------------------------------------------------------------- # Fitbit -- optional integration (classic Web API, per-user BYO-keys) # ----------------------------------------------------------------------------- # Each user registers their own app at https://dev.fitbit.com (self-serve, no # brand verification) and pastes the OAuth Client ID + Client Secret into # Settings (stored encrypted in the DB). There is therefore no # FITBIT_CLIENT_ID/SECRET here. The OAuth flow uses Authorization Code + PKCE. # # FITBIT_REDIRECT_URI overrides the derived # `${NEXT_PUBLIC_APP_URL}/api/fitbit/callback` when the public URL differs; it # must exactly match the Callback URL registered in the dev.fitbit.com app. # FITBIT_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="" # ----------------------------------------------------------------------------- # Strava -- optional integration (full OAuth, shared app from env) # ----------------------------------------------------------------------------- # Register an app at https://www.strava.com/settings/api and set its # "Authorization Callback Domain" to your instance host. Both vars together # enable the Strava connect button; set neither to leave the integration # disabled. Per-user BYO client id/secret in Settings take precedence over these # shared env values. Strava caps every new app at single-player mode (athlete # capacity 1) until you request a capacity increase — fine for a personal # self-host. Strava rotates its refresh token on every refresh; the sync # refreshes reactively on a 401. STRAVA_REDIRECT_URI overrides the derived # `${NEXT_PUBLIC_APP_URL}/api/strava/callback`. # STRAVA_CLIENT_ID="" # STRAVA_CLIENT_SECRET="" # STRAVA_REDIRECT_URI="" # ----------------------------------------------------------------------------- # OIDC SSO login -- optional (self-hosted identity provider) # ----------------------------------------------------------------------------- # Register a confidential OIDC client for your IdP (Authentik, Keycloak, # Authelia, Google Workspace, etc.) with redirect URI # `${NEXT_PUBLIC_APP_URL}/api/auth/oidc/callback`. All three of ISSUER_URL/ # CLIENT_ID/CLIENT_SECRET together enable the "Sign in with SSO" button on # the login page; set none to leave it disabled. First-time sign-in # auto-provisions (or links, by verified email) a HealthLog account. OIDC_ONLY # hides password/passkey login entirely once set to "true" -- but only takes # effect when the three vars above are also fully set, so a typo can't lock # every user out. Enforced server-side, not just a hidden button. The native # iOS client signs in through the native OIDC SSO handoff (the app opens the # login URL in an in-app browser and completes at a custom-scheme callback), so # OIDC_ONLY="true" is supported for iOS -- but only on the app build that ships # that flow. An older password/passkey-only iOS build is locked out of sign-in # by OIDC_ONLY; confirm the app version before enabling it on a deployment with # active iOS users. # Serve the issuer over HTTPS wherever possible: sign-in material (the # authorization code, the token-endpoint response carrying the ID token) # crosses this connection. Plain HTTP is defensible only on a fully trusted # private network (IdP and app on the same box, a Tailscale mesh, a VPN). # OIDC_ISSUER_URL="" # OIDC_CLIENT_ID="" # OIDC_CLIENT_SECRET="" # OIDC_SCOPES="openid email profile" # OIDC_BUTTON_LABEL="Single Sign-On" # OIDC_ONLY="false" # ----------------------------------------------------------------------------- # 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). These three # names are the only ones it reads, and all three are on docker-compose.yml's # `environment:` whitelist, so a value set here reaches the container. # 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 # APNS_PRODUCTION="" # "true" forces the production APNs gateway; normally derived from NODE_ENV # ----------------------------------------------------------------------------- # 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" # ----------------------------------------------------------------------------- # Session cookie Secure flag -- the most common self-host snag # ----------------------------------------------------------------------------- # Unset, the flag tracks NODE_ENV, so a production image sets `Secure` and a # browser on plain HTTP silently drops the session cookie: login appears to # succeed and every following request is anonymous. Set it to false ONLY on a # LAN, Tailscale or VPN-only host that deliberately serves HTTP. Behind a TLS # reverse proxy, leave it alone. # SESSION_COOKIE_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="" # Local-AI SSRF opt-in (optional). The local AI provider rejects private/ # internal hosts by default. Set to an exact host allowlist (comma-separated, # e.g. "ollama.lan,10.0.0.5") or "true" for any private host — intended for a # self-hosted Ollama / LM Studio endpoint. See docs/integrations/ai-providers.md. # ALLOW_LOCAL_AI_PRIVATE_HOSTS="" # Daily-briefing output-token ceiling (optional). Default 2500, clamped to # 500-8000. Raise it when a verbose model's briefing JSON gets cut off # ("AI response was cut off"). See docs/integrations/ai-providers.md. # INSIGHTS_MAX_TOKENS="" # Per-user hourly ceiling for POST /api/insights/generate (optional). # Default 10. Lower it on a tight LLM budget; the 24h cache already # short-circuits read traffic. See docs/integrations/ai-providers.md. # INSIGHTS_RATE_LIMIT_PER_HOUR="" # Per-user hourly ceiling for the document-AI actions (optional). Read/index, # extract, suggest and summarise share one bucket per user. Default 6, # clamped to 1-1000. Raise it when users batch-read many documents against a # generous provider budget; lower it on a tight one. See # docs/integrations/ai-providers.md. # DOCUMENT_AI_LIMIT_PER_HOUR="" # Extra hosts the ADMIN-configured AI base URL may point at, on top of the # built-in public providers (comma-separated bare hostnames). Set it when you # run a gateway this instance should trust. It widens the admin setting only; # per-user BYO keys keep the pinned public-host posture either way. # ADMIN_AI_BASE_URL_ALLOWLIST="" # Codex (ChatGPT sign-in) model slugs. CODEX_MODEL pins one slug ahead of the # built-in chain; CODEX_MODEL_FALLBACK_CHAIN replaces the chain outright # (comma-separated, first match wins). Reach for these when the provider # rotates a slug and requests start failing with 503 -- without them a # rotation needs a new release to fix. # CODEX_MODEL="" # CODEX_MODEL_FALLBACK_CHAIN="" # Surfaces reviewed n-of-1 experiment outcomes in the Coach snapshot. Off by # default: the read-back is the most overclaim-prone Coach surface, so it is a # deliberate operator opt-in. Set to "1" to enable; anything else keeps it # dormant while the review worker still records outcomes. # COACH_EXPERIMENT_VERDICT="1" # ----------------------------------------------------------------------------- # Off-host backups -- all-or-none # ----------------------------------------------------------------------------- # BACKUP_S3_ENDPOINT="" # BACKUP_S3_BUCKET="" # BACKUP_S3_ACCESS_KEY="" # BACKUP_S3_SECRET_KEY="" # BACKUP_ENCRYPTION_KEY="" # The two optional members of the set, both with defaults, so leaving them # unset does not disable backups. # # Region sent with the SigV4 signature. Default "auto", which is what # Cloudflare R2 expects; AWS S3 and most MinIO deployments want their real # region name here. # BACKUP_S3_REGION="auto" # # How long backup objects are meant to live, in days. Default 30, minimum 1. # The worker never deletes a BACKUP object -- its grant is PutObject, GetObject # and AbortMultipartUpload -- so this number is what you set the bucket's own # lifecycle rule to. The one key it does delete is its own connection test's # `_healthcheck/.bin`, and that delete is best-effort: without DeleteObject # on that prefix every Test-connection press leaves one 1-byte orphan behind, # silently. See docs/ops/backup-restore.md. # BACKUP_RETENTION_DAYS="30" # ----------------------------------------------------------------------------- # Audit-trail retention -- optional # ----------------------------------------------------------------------------- # How long rows in `audit_logs` survive, in days. Default 365. The floor is 7: # a smaller value is ignored rather than obeyed, so a window entered in seconds # by mistake cannot empty a fresh table. A daily job does the pruning. # # This number is shown to users, not just enforced. Settings -> Shared access # prints the window under the activity list, and the dialog that ends somebody # else's access states how long "who entered what" stays answerable. Both take # the value the server resolved from this variable, so an instance running 90 # says 90 on both surfaces. # AUDIT_LOG_RETENTION_DAYS="365" # How long Coach conversations survive, in days. Default 365, floor 30. Like # the audit window above this number is shown to the user -- the Data & # Privacy page states it -- so an instance running 90 has to be able to say # 90 rather than repeat the default. # COACH_MESSAGE_RETENTION_DAYS="365" # How long the host CPU/memory samples behind the admin monitoring charts are # kept, in days. Default 7. Raise it for a longer trend; lower it on a host # where disk is the scarce resource. # HOST_METRIC_RETENTION_DAYS="7" # Thinning of dense intraday samples (heart rate, glucose) once they age out # of the detail window. On by default. Set "false" to keep every raw sample # forever -- accurate to the second, at the cost of unbounded table growth. # DENSE_INTRADAY_RETENTION_ENABLED="false" # Consecutive failures before an integration is reported as persistently # failing and notified about. Default 3, minimum 1. Raise it when a flaky # upstream makes the notification noisy. # INTEGRATION_FAILURE_ALERT_THRESHOLD="3" # ----------------------------------------------------------------------------- # Database session timeout -- optional # ----------------------------------------------------------------------------- # Statement timeout applied to every database session, in milliseconds. # Default 60000, which bounds a genuinely runaway query while leaving headroom # for the heaviest legitimate read (the live-aggregate analytics fallback on a # large account). Set "0" to disable it and restore unbounded behaviour. # DATABASE_STATEMENT_TIMEOUT_MS="60000" # ----------------------------------------------------------------------------- # 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" # ----------------------------------------------------------------------------- # Client-IP resolution -- optional (required behind Cloudflare) # ----------------------------------------------------------------------------- # When the stack sits behind Cloudflare, the real visitor IP arrives in the # `cf-connecting-ip` header; the X-Forwarded-For chain otherwise yields the # Cloudflare edge IP, so the audit-log overview stores the CF datacentre # address and the geo lookup resolves the wrong location. Set this to "1" # ONLY when Cloudflare is in front -- a direct-exposed self-host must leave # it unset, or an attacker could forge the header. Off by default. # Number of trusted reverse-proxy hops in front of the app; the client IP is # read that many entries from the right of the X-Forwarded-For chain. Unset # for a directly-exposed host, 1 behind a single reverse proxy. A wrong # value makes the app refuse XFF and collapse anonymous callers into one # rate-limit bucket. # TRUST_PROXY_HOPS="1" # TRUST_CF_CONNECTING_IP="1" # ----------------------------------------------------------------------------- # IP geolocation lookup -- optional # ----------------------------------------------------------------------------- # The admin login/audit overview enriches auth events with a coarse # "City, CC" location + carrier. The default online provider is ipwho.is # (free, no key, HTTPS). Override the base URL to point at a different # provider; the response must match either the ipwho.is shape (`success` + # `country_code`) or the ip-api.com shape (`status` + `countryCode`). The # `/` segment is appended automatically. # IP_GEO_LOOKUP_URL="https://ipwho.is" # # By default a non-HTTPS URL is refused (audit-event IPs must never egress over # plaintext HTTP). The free ip-api.com endpoint is HTTP-only (its HTTPS form # needs a paid key) but is often more accurate. To use it, set BOTH the HTTP # URL and the explicit opt-in below -- the looked-up IP then travels in clear # over your server's own egress; this is your call to make. # IP_GEO_LOOKUP_URL="http://ip-api.com/json" # IP_GEO_ALLOW_INSECURE="true" # # Set to "1" to disable all third-party IP egress (GDPR Art. 44). With egress # off the resolver uses only the offline GeoLite2 MMDB tier, if present. # IP_GEO_LOOKUP_DISABLED="1" # ----------------------------------------------------------------------------- # Offline GeoLite2 databases -- optional # ----------------------------------------------------------------------------- # Where the runtime looks for GeoLite2-City.mmdb and GeoLite2-ASN.mmdb. # Unset it reads /opt/geolite2, the path the published image bakes its own # copy into -- but only if the image build had a MaxMind licence key, so on # a stock `:latest` the directory is usually empty and every login IP goes # to the online provider instead. # # Three ways to get the databases: # # 1. RUNTIME FETCH (issue #659) -- set MAXMIND_LICENSE_KEY below and the # worker downloads both databases into GEOLITE2_DIR itself on the next # boot and monthly thereafter. No image rebuild, no manual mount. This is # the way for a stock `:latest` pull. # 2. Bring your own -- create a free MaxMind account, pull both databases # with `geoipupdate`, mount the directory into the container read-only # (the commented `volumes:` block in docker-compose.yml shows the line), # and point GEOLITE2_DIR at the container path. Refreshing the files on # the host is enough -- the reader picks them up on the next restart. # 3. Bake them at build time (build your own image with a licence key). # # Pair any of them with IP_GEO_LOOKUP_DISABLED="1" above for zero IP egress. # See docs/self-hosting/geolite2.md. # GEOLITE2_DIR="/var/lib/geolite2" # # MaxMind licence key for the runtime fetch (option 1 above). Free: sign up at # https://www.maxmind.com/en/geolite2/signup, then My Account -> Manage # License Keys. Unset, nothing is fetched and the online provider answers. # MAXMIND_LICENSE_KEY="your-maxmind-licence-key" # ----------------------------------------------------------------------------- # Environmental-context module (Open-Meteo) -- optional # ----------------------------------------------------------------------------- # The opt-in Environment module fetches daily weather / daylight for the user's # coarse home (or a travel override) from Open-Meteo. The hosted endpoints are # keyless and free for non-commercial use; data is CC BY 4.0. Both URLs are # optional -- unset uses the hosted defaults below. A privacy-maximalist or # commercial self-hoster can run their own Open-Meteo instance and point these # at it (a private/LAN host is allowed -- the fetch is server-side and does NOT # force the public-host guard, so an internal instance is not blocked). # OPENMETEO_BASE_URL="https://archive-api.open-meteo.com" # OPENMETEO_GEOCODING_URL="https://geocoding-api.open-meteo.com" # ── Native canvas renderer (document thumbnails, scanned-PDF rasterization) ── # The bundled renderer's x64 build uses AVX2 CPU instructions. On x86-64 hosts # WITHOUT AVX2 (Celeron/Atom-class NAS CPUs) it is disabled automatically and # those features degrade cleanly (no thumbnails; scanned PDFs read as text # only). Override only if auto-detection misfires: # off — force-disable the renderer # on — force-enable (crashes the container if the CPU truly lacks AVX2) # NATIVE_CANVAS="off" # ----------------------------------------------------------------------------- # Logging -- optional # ----------------------------------------------------------------------------- # LOG_LEVEL is one of debug / info / warn / error. Default info: drop to debug # while chasing a problem, raise to warn on a busy instance. LOG_SAMPLE_RATE # keeps that fraction of SUCCESSFUL events (default 1.0 = all; 0.1 thins a # chatty log without losing errors). LOG_SLOW_THRESHOLD_MS is the duration # above which a request is flagged slow (default 3000). LOG_INCLUDE_STACK set # to "false" strips stack traces from the emitted events. # LOG_LEVEL="info" # LOG_SAMPLE_RATE="1.0" # LOG_SLOW_THRESHOLD_MS="3000" # LOG_INCLUDE_STACK="false" # Optional log shipping to a Loki instance. LOKI_ENDPOINT is the push URL and # is what turns shipping on; the username/password pair is only needed when # your Loki sits behind basic auth. Leave all three unset and logs stay on # stdout for the Docker log driver to rotate. # LOKI_ENDPOINT="" # LOKI_USERNAME="" # LOKI_PASSWORD="" # ----------------------------------------------------------------------------- # Demo mode -- read-only showcase instance # ----------------------------------------------------------------------------- # "true" makes the proxy refuse every mutation except login and paints a demo # banner. This is the switch behind a public try-it-out deployment, not a # safety belt for a real one -- leave it unset on an instance holding real # data. # DEMO_MODE="true" # ----------------------------------------------------------------------------- # Server-side prefetch -- diagnostics escape hatch # ----------------------------------------------------------------------------- # The dashboard, insights, coach and checkups pages prefetch their data on the # server so the first paint arrives filled in. Set "false" to fall back to # pure client fetching. Reach for it only when a prefetch is misbehaving on # your host and you want to see whether the client path is healthy. # DASHBOARD_SSR_PREFETCH="false"