mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
305 lines
14 KiB
JSON
305 lines
14 KiB
JSON
{
|
|
"$schema": "./env-manifest.schema.json",
|
|
"description": "Manifest of env vars required by a HealthLog production deployment. Used by `pnpm check-env` to catch missing/empty vars BEFORE a deploy ships. v1.4.42 W6 \u2014 born out of the v1.4.40 AP-2 .p8 install gap that shipped silently for 3 days.",
|
|
"groups": [
|
|
{
|
|
"name": "Core",
|
|
"description": "Database, app URL, encryption keys. The app refuses to boot without these.",
|
|
"required": true,
|
|
"variables": [
|
|
{
|
|
"name": "DATABASE_URL",
|
|
"purpose": "Postgres connection string. App boot fails if missing.",
|
|
"example": "postgresql://healthlog:<pw>@db:5432/healthlog?schema=public"
|
|
},
|
|
{
|
|
"name": "POSTGRES_PASSWORD",
|
|
"purpose": "Database password for the bundled `db` service in docker-compose.yml.",
|
|
"example": "<openssl rand -hex 32>"
|
|
},
|
|
{
|
|
"name": "NEXT_PUBLIC_APP_URL",
|
|
"purpose": "Public URL embedded in OAuth redirects, webhook callbacks, APNs deep-links."
|
|
},
|
|
{
|
|
"name": "APP_URL",
|
|
"purpose": "Server-side mirror of NEXT_PUBLIC_APP_URL; used by background workers."
|
|
},
|
|
{
|
|
"name": "ENCRYPTION_KEY",
|
|
"purpose": "AES-256-GCM key for at-rest encryption of provider tokens. 64 hex chars (32 bytes). Required unless ENCRYPTION_KEYS is set."
|
|
},
|
|
{
|
|
"name": "API_TOKEN_HMAC_KEY",
|
|
"purpose": "HMAC-SHA256 key for hashing external Bearer tokens AND web session cookies. 64 hex chars. Rotating it signs every browser session out on top of invalidating every issued token."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Withings OAuth",
|
|
"description": "Withings API credentials are entered per user in Settings \u2192 Integrations (no env-var path). Only the webhook shared secret is operator-level.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "WITHINGS_WEBHOOK_SECRET",
|
|
"purpose": "Random secret carried as a path segment in the Withings webhook callback URL. Generate with `openssl rand -hex 32`."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Polar OAuth",
|
|
"description": "Required to enable the Polar AccessLink integration. App boots without these but the Polar connect button stays disabled. Register an app at https://admin.polaraccesslink.com and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/polar/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "POLAR_CLIENT_ID",
|
|
"purpose": "Polar AccessLink OAuth client id."
|
|
},
|
|
{
|
|
"name": "POLAR_CLIENT_SECRET",
|
|
"purpose": "Polar AccessLink OAuth client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Oura OAuth",
|
|
"description": "Required to enable the Oura Cloud v2 integration. App boots without these but the Oura connect button stays disabled. Register an app at https://cloud.ouraring.com/oauth/applications and set the redirect URI to ${NEXT_PUBLIC_APP_URL}/api/oura/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "OURA_CLIENT_ID",
|
|
"purpose": "Oura Cloud OAuth client id."
|
|
},
|
|
{
|
|
"name": "OURA_CLIENT_SECRET",
|
|
"purpose": "Oura Cloud OAuth client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "OIDC SSO login",
|
|
"description": "Required to enable SSO login against a self-hosted identity provider (Authentik, Keycloak, Authelia, Google Workspace, etc.). App boots without these but the login page's SSO button stays hidden. Register a confidential client with redirect URI ${NEXT_PUBLIC_APP_URL}/api/auth/oidc/callback.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "OIDC_ISSUER_URL",
|
|
"purpose": "OIDC provider issuer URL (discovery doc served at <issuer>/.well-known/openid-configuration)."
|
|
},
|
|
{
|
|
"name": "OIDC_CLIENT_ID",
|
|
"purpose": "OIDC confidential client id."
|
|
},
|
|
{
|
|
"name": "OIDC_CLIENT_SECRET",
|
|
"purpose": "OIDC confidential client secret."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "OIDC SSO options",
|
|
"description": "Optional tuning for an enabled OIDC provider. Each stands alone (deliberately NOT allOrNone \u2014 any subset is valid) and each has a safe default when unset.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "OIDC_SCOPES",
|
|
"purpose": "Space-separated scopes requested at authorization. Default: \"openid email profile\"."
|
|
},
|
|
{
|
|
"name": "OIDC_BUTTON_LABEL",
|
|
"purpose": "Label on the login page's SSO button. Default: \"Single Sign-On\"."
|
|
},
|
|
{
|
|
"name": "OIDC_ONLY",
|
|
"purpose": "\"true\" disables password/passkey login server-side (only effective when the OIDC provider group is fully configured). Default: additive login. Locks the native iOS app out of sign-in."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "AI Provider",
|
|
"description": "Optional runtime policy and output tuning for user-configured AI providers. Each variable stands alone and has a safe default when unset.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "ALLOW_LOCAL_AI_PRIVATE_HOSTS",
|
|
"purpose": "Local-AI SSRF opt-in. Unset rejects private/internal hosts; set an exact comma-separated host allowlist or \"true\" to allow any private host."
|
|
},
|
|
{
|
|
"name": "INSIGHTS_MAX_TOKENS",
|
|
"purpose": "Daily-briefing output-token ceiling. Default 2500, clamped to 500-8000."
|
|
},
|
|
{
|
|
"name": "INSIGHTS_RATE_LIMIT_PER_HOUR",
|
|
"purpose": "Per-user hourly ceiling for POST /api/insights/generate. Default 10; values below 1 fall back to the default."
|
|
},
|
|
{
|
|
"name": "DOCUMENT_AI_LIMIT_PER_HOUR",
|
|
"purpose": "Per-user hourly ceiling for the document-AI actions (read/index, extract, suggest, summarise share one bucket). Default 6, clamped to 1-1000."
|
|
},
|
|
{
|
|
"name": "DOCUMENT_UPLOAD_LIMIT_PER_HOUR",
|
|
"purpose": "Hourly upload ceiling per document-upload token (Paperless workflow, import script). Default 120, clamped to 1-1000. Web and phone uploads keep their own bucket."
|
|
},
|
|
{
|
|
"name": "LABS_OCR_LIMIT_PER_HOUR",
|
|
"purpose": "Per-user hourly ceiling for lab-report scans (photo/PDF and browser-OCR text share one bucket). Default 6, clamped to 1-1000."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Offline geolocation",
|
|
"description": "Optional offline GeoLite2 tier for the admin login overview. Unset, the runtime reads /opt/geolite2 (the path the image bakes its own copy into) and falls back to the online provider when the databases are absent.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "GEOLITE2_DIR",
|
|
"purpose": "Directory holding GeoLite2-City.mmdb + GeoLite2-ASN.mmdb. Point it at a read-only mount to bring your own databases. Default /opt/geolite2."
|
|
},
|
|
{
|
|
"name": "MAXMIND_LICENSE_KEY",
|
|
"purpose": "MaxMind licence key for the runtime GeoLite2 fetch (issue #659). Set it and the worker downloads both databases into GEOLITE2_DIR on boot and monthly, no rebuild or manual mount. Free at maxmind.com/en/geolite2/signup."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "APNs (iOS push)",
|
|
"description": "All four core APNS_* vars together. Missing any disables APNs silently \u2014 the v1.4.40 AP-2 gap that motivated this manifest. allOrNone trips the synthetic <all-or-none> required row whenever 1-3 of the 4 are set.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "APNS_KEY_ID",
|
|
"purpose": "10-char Key ID from Apple Developer."
|
|
},
|
|
{
|
|
"name": "APNS_TEAM_ID",
|
|
"purpose": "10-char Team ID from Apple Developer."
|
|
},
|
|
{
|
|
"name": "APNS_BUNDLE_ID",
|
|
"purpose": "iOS app bundle id, e.g. dev.healthlog.ios."
|
|
},
|
|
{
|
|
"name": "APNS_KEY",
|
|
"purpose": "APNs auth key (.p8 contents). Use APNS_KEY for 12-factor env-block deploys; APNS_KEY_FILE is the filesystem alternative \u2014 at least one of the two must be set.",
|
|
"anyOf": ["APNS_KEY", "APNS_KEY_FILE"]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Deploy webhook",
|
|
"description": "Coolify -> HealthLog deploy-result webhook. Without this, the auto-deploy success/failure feedback loop is broken.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "DEPLOY_WEBHOOK_SECRET",
|
|
"purpose": "Shared secret for the X-Deploy-Webhook-Secret header on POST /api/internal/deploy-webhook. 64 hex chars."
|
|
},
|
|
{
|
|
"name": "DEPLOY_LOGS_URL",
|
|
"purpose": "URL of the deployment dashboard (e.g. the Coolify UI) linked in deploy-failure notifications. Optional; when unset the notification omits the link."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "TLS leaf pin monitor",
|
|
"description": "Baseline for the TLS leaf SPKI-change alarm. The native client pins the served leaf certificate; without this the monitor logs the served pin but cannot alarm on a rotation. See docs/ops/tls-cert-pin.md.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "TLS_LEAF_SPKI_PINS",
|
|
"purpose": "Comma-separated known-good leaf SPKI pins, base64(sha256(DER subjectPublicKeyInfo)) \u2014 the same value the native client pins. Hold both the current and the next pin during a dual-pin renewal window."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "SMTP (email channel)",
|
|
"description": "Operator SMTP transport for the email notification channel (v1.17.1). All of SMTP_HOST, SMTP_PORT, SMTP_FROM together enable the channel; SMTP_USER/SMTP_PASS are optional (unauthenticated relay). Missing any of the three core vars disables email silently \u2014 the channel is never offered and the Settings card hides itself.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "SMTP_HOST",
|
|
"purpose": "SMTP server hostname, e.g. smtp.example.com."
|
|
},
|
|
{
|
|
"name": "SMTP_PORT",
|
|
"purpose": "SMTP port. 587 for STARTTLS (SMTP_SECURE unset/false), 465 for implicit TLS (SMTP_SECURE=true)."
|
|
},
|
|
{
|
|
"name": "SMTP_FROM",
|
|
"purpose": "From address for outbound mail, e.g. \"HealthLog <noreply@example.com>\"."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Notification targets on a private network",
|
|
"description": "Operator grant for webhook (Gotify, Home Assistant, n8n) and ntfy targets on the operator's own network. Unset, a target that resolves to a private address is refused at send time and the test button reports private_origin_not_approved. Optional; public targets never need it.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "NOTIFICATION_PRIVATE_ORIGINS",
|
|
"purpose": "Comma-separated exact origins (scheme://host[:port], no path, no wildcard, no CIDR), e.g. \"https://gotify.example.com,http://ntfy.lan:8080\". Link-local, metadata and unspecified addresses stay refused even when listed; a malformed entry is logged once and grants nothing."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Off-host backups",
|
|
"description": "Required ALL-OR-NONE: any of these without the rest disables backups silently.",
|
|
"required": false,
|
|
"allOrNone": true,
|
|
"variables": [
|
|
{
|
|
"name": "BACKUP_S3_ENDPOINT",
|
|
"purpose": "S3-compatible endpoint URL."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_BUCKET",
|
|
"purpose": "Bucket name."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_ACCESS_KEY",
|
|
"purpose": "S3 access key."
|
|
},
|
|
{
|
|
"name": "BACKUP_S3_SECRET_KEY",
|
|
"purpose": "S3 secret key."
|
|
},
|
|
{
|
|
"name": "BACKUP_ENCRYPTION_KEY",
|
|
"purpose": "Separate AES key for the backup body \u2014 MUST NOT match ENCRYPTION_KEY."
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Operational windows and cookie flags",
|
|
"description": "Values the product states back to a user or that decide whether a session cookie is accepted. Optional: each has a documented default. They are listed so `pnpm check-env` reports them, and so the compose whitelist and the example files can be held in step with this manifest.",
|
|
"required": false,
|
|
"variables": [
|
|
{
|
|
"name": "AUDIT_LOG_RETENTION_DAYS",
|
|
"purpose": "How long audit rows survive, in days (365 by default). Published, not merely enforced: the shared-access activity feed and the dialog that ends somebody's access both state the number back to the user.",
|
|
"example": "365"
|
|
},
|
|
{
|
|
"name": "SESSION_COOKIE_SECURE",
|
|
"purpose": "Forces the Secure flag on every cookie the app issues. Unset, it tracks NODE_ENV. Set false for a LAN / VPN-only self-host served over plain HTTP, where the default makes login look broken.",
|
|
"example": "false"
|
|
},
|
|
{
|
|
"name": "TRUST_PROXY_HOPS",
|
|
"purpose": "Trusted reverse-proxy hop count; the client IP is read that many entries from the right of X-Forwarded-For.",
|
|
"example": "1"
|
|
},
|
|
{
|
|
"name": "BACKUP_MAX_STORED_MB",
|
|
"purpose": "Largest stored backup of one account the app keeps, in megabytes of encrypted copy (2048 by default). A storage limit, independent of the app's memory: a backup past it fails with a message naming this variable, and the previous copy stays.",
|
|
"example": "2048"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|