# ============================================================================= # HealthLog — Environment Configuration # ============================================================================= # Copy this file to .env and fill in the values. # $ cp .env.example .env # # Required variables: DATABASE_URL or POSTGRES_PASSWORD, # ENCRYPTION_KEY, API_TOKEN_HMAC_KEY # # Generate the two secrets with: # $ openssl rand -hex 32 # any UNIX shell # $ node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))' # # All secrets must be 64-character hex strings (= 32 bytes). # ============================================================================= # ----------------------------------------------------------------------------- # Database # ----------------------------------------------------------------------------- # When using docker-compose.yml: the app container reaches Postgres via the # service name "db", so the hostname is "db" — NOT "localhost". # When developing without Docker (`pnpm dev` against a host-installed Postgres): # change the hostname to "localhost" and adjust the credentials. DATABASE_URL="postgresql://healthlog:CHANGE-ME@db:5432/healthlog?schema=public" # Postgres password — used both by the db service AND by DATABASE_URL above. # Pick a strong random password, e.g. `openssl rand -base64 24`. POSTGRES_PASSWORD="CHANGE-ME" # ----------------------------------------------------------------------------- # App URL # ----------------------------------------------------------------------------- # The public URL where users reach the app. Behind a reverse proxy with TLS, # this looks like https://healthlog.example.com. For local dev, leave default. NEXT_PUBLIC_APP_URL="http://localhost:3000" APP_URL="http://localhost:3000" # Node environment: "development" for `pnpm dev`, "production" for Docker. NODE_ENV="development" # ----------------------------------------------------------------------------- # Required Secrets — generate with `openssl rand -hex 32` # ----------------------------------------------------------------------------- # AES-256-GCM key for encrypting Withings tokens, GitHub PATs, AI provider # keys, and VAPID private keys at rest. Rotate carefully — old ciphertexts # need re-encryption. ENCRYPTION_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" # HMAC-SHA256 key for hashing external-API Bearer tokens before storing them # in the database. Rotating it invalidates all issued tokens. API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" # ----------------------------------------------------------------------------- # Withings OAuth (optional — for automatic scale/BP/activity sync) # ----------------------------------------------------------------------------- # Register an app at https://developer.withings.com/ and paste the credentials # here. Per-user OAuth credentials can also be configured in Settings. # WITHINGS_CLIENT_ID="" # WITHINGS_CLIENT_SECRET="" # WITHINGS_REDIRECT_URI="https://healthlog.example.com/api/withings/callback" # WITHINGS_WEBHOOK_SECRET="" # any random string — used to harden the webhook URL # ----------------------------------------------------------------------------- # Telegram (optional — webhook hardening) # ----------------------------------------------------------------------------- # Set to verify incoming Telegram updates. The bot token itself is configured # per-user in Settings (encrypted at rest). # TELEGRAM_WEBHOOK_SECRET="" # ----------------------------------------------------------------------------- # AI Provider (optional) # ----------------------------------------------------------------------------- # Users bring their own API keys via Settings. No server-side key needed by # default. The admin can configure a fallback key in the Admin Panel. # Maximum number of comprehensive-insight generations a single user can # trigger per hour (POST /api/insights/generate). Default 10. The 24h # DB cache short-circuits this for read traffic; only force=true and # expired-cache regenerations consume tokens. Lower this when running # on a tight LLM budget. # INSIGHTS_RATE_LIMIT_PER_HOUR="10" # ----------------------------------------------------------------------------- # Structured Logging — Wide Events (optional, sensible defaults) # ----------------------------------------------------------------------------- # Levels: debug | info | warn | error # LOG_LEVEL="info" # Sample rate for successful requests (0.0 = sample nothing, 1.0 = log all). # Errors and slow requests are ALWAYS logged regardless of this setting. # LOG_SAMPLE_RATE="1.0" # Slow-request threshold in milliseconds — anything slower is always kept. # LOG_SLOW_THRESHOLD_MS="3000" # Include stack traces on error events. Disable in environments where logs # leave the host unencrypted. # LOG_INCLUDE_STACK="true" # Optional: ship Wide Events to Loki. Leave blank for stdout-only logging. # LOKI_ENDPOINT="http://loki:3100" # LOKI_USERNAME="" # LOKI_PASSWORD="" # ----------------------------------------------------------------------------- # Process split (optional, single-container deploys ignore this) # ----------------------------------------------------------------------------- # `all` — single container runs HTTP + reminder worker (default, legacy). # `web` — HTTP only; pair with a separate `app-worker` service. # `worker`— pg-boss only; rejects HTTP traffic with a 503. # See docs/self-hosting/scaling.md for the split-stack guide. # HEALTHLOG_PROCESS_TYPE="all" # ----------------------------------------------------------------------------- # Encryption-key versioning (optional — for at-rest key rotation) # ----------------------------------------------------------------------------- # JSON map of key id → 64-hex-char or base64 32-byte key. When set, it # supersedes ENCRYPTION_KEY for new writes; old rows continue to decrypt under # their original id. ENCRYPTION_ACTIVE_KEY_ID picks which entry encrypt() uses. # Run `pnpm tsx scripts/rotate-encryption-key.ts ` to re-encrypt every # existing row under the new key. See docs/ops/encryption-key-rotation.md. # ENCRYPTION_KEYS='{"v1":"","v2":""}' # ENCRYPTION_ACTIVE_KEY_ID="v2" # ----------------------------------------------------------------------------- # Off-host backup (optional — daily encrypted dumps to an S3-compatible bucket) # ----------------------------------------------------------------------------- # Required ALL-OR-NONE: setting any of these without setting the rest disables # the backup silently. The backup runs at 02:30 Europe/Berlin via pg-boss cron. # Restore with `pnpm tsx scripts/restore-backup.ts `. # See docs/ops/backup-restore.md for the format spec + step-by-step. # BACKUP_S3_ENDPOINT="https://.r2.cloudflarestorage.com" # BACKUP_S3_BUCKET="healthlog-backups" # BACKUP_S3_ACCESS_KEY="" # BACKUP_S3_SECRET_KEY="" # BACKUP_S3_REGION="auto" # "auto" for Cloudflare R2; real region for AWS S3 # BACKUP_RETENTION_DAYS="30" # objects older than this are pruned (best-effort; prefer a bucket lifecycle rule) # BACKUP_ENCRYPTION_KEY="" # 64-hex-char or base64 32-byte; SEPARATE from ENCRYPTION_KEY # ----------------------------------------------------------------------------- # APNs — Apple Push Notification service (v1.4.23+, optional) # ----------------------------------------------------------------------------- # All four core APNS_* vars must be set together for APNs to fan out to iOS # devices. Leaving them unset disables the channel silently — the dispatcher # falls back to Telegram → ntfy → Web Push as before. # # Provision an APNs auth key (.p8) at https://developer.apple.com/account/ # under Certificates, Identifiers & Profiles → Keys → APNs. # # APNS_KEY (preferred for 12-factor / Coolify env-block) and APNS_KEY_FILE # (filesystem path) are mutually exclusive — pick one. APNS_KEY accepts the # multi-line PEM body with literal newlines OR `\n` escapes. # APNS_KEY_ID="" # 10-char Key ID from Apple Developer (e.g. ABCDE12345) # APNS_TEAM_ID="" # 10-char Team ID from Apple Developer # APNS_BUNDLE_ID="" # iOS app bundle id (e.g. dev.healthlog.ios) # APNS_KEY="" # APNs auth key as PEM body (multi-line OK with \n escapes); preferred over APNS_KEY_FILE # APNS_KEY_FILE="" # Alternate: filesystem path to .p8 file # APNS_PRODUCTION="" # "true" to force the production gateway; default = mirror Device.apnsEnvironment # ----------------------------------------------------------------------------- # Auto-deploy webhook (v1.4.15 — phase C2) # ----------------------------------------------------------------------------- # Coolify (or any CI) pings POST /api/internal/deploy-webhook after a deploy # attempt with success/failure. We verify the request via this shared secret # carried in the `X-Deploy-Webhook-Secret` header (timing-safe compare). # Generate with `openssl rand -hex 32`. Configure the SAME value in Coolify's # Notifications → Webhook custom-headers panel. See docs/audit/v1415-auto-deploy.md. # DEPLOY_WEBHOOK_SECRET=""