# Required in all deployments: set one strong secret source before first start. # SECRET_KEY takes precedence if both are set. # SECRET_KEY_FILE must point to a readable in-container path, for example /run/secrets/ovumcy_secret_key. # Rotating this secret is planned maintenance, not a routine change: it invalidates # every session, TOTP secret, stored webhook URL, and calendar-feed subscribe URL. SECRET_KEY= SECRET_KEY_FILE= # Core behavior (recommended defaults) TZ=UTC # Supported: en, ru, es, fr, de, it DEFAULT_LANGUAGE=en REGISTRATION_MODE=open DB_DRIVER=sqlite DB_PATH=data/ovumcy.db # CALENDAR_FEED_FENCE_PATH — the calendar-feed restore fence. It has to sit # somewhere your database backups do NOT capture: that is how a restored backup # is told apart from the database it replaced, so a feed an owner revoked cannot # come back live at its old URL. No value here on purpose. Under the bundled # compose files there is nothing to set — they mount ovumcy_fence and pass the # variable under `environment:`, which overrides this file anyway. Running the # binary yourself, set it in that process's environment to a file in a directory # your backups skip; a container path copied from a compose file will not exist. # Use an absolute path. The server resolves a relative one against its own # working directory, which is not the operator CLI's; the CLI does not resolve # one at all — `ovumcy reset-password` and `ovumcy users delete` refuse outright # on a relative value, because no shell can prove it names the server's own file. # With no fence behind it the app fails closed: every armed calendar feed is # disarmed on each start, and the startup log says so. # Internal app port. Public HTTPS deployments should expose only the proxy. PORT=8080 # Host side of the base compose port publish. Only compose reads it: the app # itself listens on PORT on every interface. HOST_BIND_ADDRESS=127.0.0.1 # Local/private base compose profile COOKIE_SECURE=false TRUST_PROXY_ENABLED=false PROXY_HEADER=X-Forwarded-For TRUSTED_PROXIES=127.0.0.1,::1 # Public reverse-proxy stack profile # Use the example stacks under docs/examples/reverse-proxy/ and adapt: # COOKIE_SECURE=true # TRUST_PROXY_ENABLED=true # PROXY_HEADER=X-Real-IP # TRUSTED_PROXIES= # Public instances should usually close open registration; create accounts via # the first-run setup or OIDC instead: # REGISTRATION_MODE=closed # Leave HOST_BIND_ADDRESS at 127.0.0.1 for the base compose path. # Strict-Transport-Security. Defaults to COOKIE_SECURE. When true, browsers pin # this host to HTTPS for a year — set false to keep secure cookies without the pin. HSTS_ENABLED= # Optional OIDC sign-in / SSO # Requires HTTPS, COOKIE_SECURE=true, and a redirect URL ending in /auth/oidc/callback. # OIDC_LOGIN_MODE: hybrid or oidc_only # OIDC_RESPONSE_MODE: form_post (default) or query. Use query only for providers that # cannot form-post the callback (Dex, better-auth, Pocket ID <2.7); the code then appears # in proxy/browser logs but is inert without the PKCE verifier. See docs/oidc.md#response-mode # OIDC_AUTO_PROVISION=true requires REGISTRATION_MODE=open and creates owner accounts without a local password first. # OIDC_AUTO_PROVISION_ALLOWED_DOMAINS accepts a comma-separated allowlist such as example.com,staff.example.com # OIDC_LOGOUT_MODE: local, provider, or auto # OIDC_CA_FILE is optional and points to a readable PEM bundle inside the container/process when your provider uses a private CA. # OIDC_CLIENT_SECRET_FILE is the Docker Swarm/Compose secrets route for the client # secret: point it at a readable in-container path instead of setting OIDC_CLIENT_SECRET # directly. OIDC_CLIENT_SECRET takes precedence if both are set. Neither is read # while OIDC_ENABLED is false. OIDC_ENABLED=false OIDC_ISSUER_URL= OIDC_CLIENT_ID= OIDC_CLIENT_SECRET= OIDC_CLIENT_SECRET_FILE= OIDC_REDIRECT_URL= OIDC_CA_FILE= OIDC_LOGIN_MODE=hybrid OIDC_RESPONSE_MODE=form_post OIDC_AUTO_PROVISION=false OIDC_AUTO_PROVISION_ALLOWED_DOMAINS= OIDC_LOGOUT_MODE=local OIDC_POST_LOGOUT_REDIRECT_URL= # Audit logging (per-action security-event log lines to stderr). # Default off; flip to true only when investigating a specific incident. # Each enabled line includes action, outcome, request method, sanitized path, # user_id, and role. Lines never leave the host; they reach the operator # through the container runtime (docker compose logs, journald). # Go panics, startup errors, and the Fiber request log remain enabled regardless. # A value other than 1/true/yes/on or 0/false/no/off refuses the boot. AUDIT_LOG_ENABLED=false # Built-in daily reminder scheduler. Default off; when enabled it makes # outbound webhook calls once per day, per owner, from this process. # REMINDER_SCHEDULER_ENABLED=false # Local hour-of-day (0-23) the daily reminder pass runs at. # REMINDER_SCHEDULER_HOUR=9 # Block outbound webhook deliveries targeting private/internal addresses # (SSRF guard for owner-configured webhooks). Off by default for local/LAN # self-hosting; consider true on public deployments. # WEBHOOK_BLOCK_PRIVATE_ADDRESSES=false # Advanced rate limits (self-host safe defaults). Each *_MAX has a ceiling (100 for # login/register/forgot-password/2fa-challenge/password-reset-redeem, 600 logout, 200 # logout account, 3000 api, 3000 calendar page, 120 calendar feed) and each *_WINDOW must # be between 1s and 24h (1m and 24h for the login, register, forgot-password, 2fa-challenge # and password-reset-redeem windows); a value outside its range is logged at boot and the # default is used instead. The login, register, forgot-password, 2fa-challenge and # password-reset-redeem pairs are also held to at most 30 requests per minute (MAX over WINDOW, e.g. 100 with # 200s); a pair above that, or an out-of-range window, is logged and both halves fall # back to the defaults. RATE_LIMIT_LOGIN_MAX=8 RATE_LIMIT_LOGIN_WINDOW=15m RATE_LIMIT_REGISTER_MAX=8 RATE_LIMIT_REGISTER_WINDOW=15m RATE_LIMIT_FORGOT_PASSWORD_MAX=8 RATE_LIMIT_FORGOT_PASSWORD_WINDOW=1h RATE_LIMIT_TOTP_CHALLENGE_MAX=8 RATE_LIMIT_TOTP_CHALLENGE_WINDOW=15m RATE_LIMIT_PASSWORD_RESET_REDEEM_MAX=8 RATE_LIMIT_PASSWORD_RESET_REDEEM_WINDOW=15m # Two budgets guard logout. The first pair is the per-IP edge limiter in front of # the endpoint; the second is the per-account, identity-keyed budget. Keep the # account one tight — several owners share one address on a household instance, # so the per-IP budget has to stay wide and cannot do that job. RATE_LIMIT_LOGOUT_MAX=60 RATE_LIMIT_LOGOUT_WINDOW=15m RATE_LIMIT_LOGOUT_ACCOUNT_MAX=20 RATE_LIMIT_LOGOUT_ACCOUNT_WINDOW=15m RATE_LIMIT_API_MAX=300 RATE_LIMIT_API_WINDOW=1m # Calendar feed (.ics): deliberately tighter than the API budget. It is the only # endpoint reachable with no credential at all, so keep it small — a calendar # client polls every 15-60 minutes, which 20/min already covers for several # devices behind one address. Never let it inherit RATE_LIMIT_API_MAX. RATE_LIMIT_CALENDAR_FEED_MAX=20 RATE_LIMIT_CALENDAR_FEED_WINDOW=1m # Calendar page (GET /calendar): authenticated, its own budget outside the API # catch-all. Its own clamp and iteration cap already bound the request's cost, # so this budget sits with the other authenticated reads. RATE_LIMIT_CALENDAR_MAX=300 RATE_LIMIT_CALENDAR_WINDOW=1m # Advanced optional Postgres path # DB_DRIVER=postgres # DATABASE_URL=postgres://ovumcy:change-me@127.0.0.1:5432/ovumcy?sslmode=disable # DATABASE_URL_FILE is the Docker Swarm/Compose secrets route for the DSN: point it at # a readable in-container path instead of setting DATABASE_URL directly. # DATABASE_URL takes precedence if both are set. # DATABASE_URL_FILE=/run/secrets/ovumcy_database_url