# Flask settings # CRITICAL: Change SECRET_KEY in production! Used for sessions, cookies, and CSRF tokens. # Generate a secure key with: python -c "import secrets; print(secrets.token_hex(32))" # The same key must be used across restarts and all app replicas. SECRET_KEY=your-secret-key-here FLASK_ENV=production FLASK_DEBUG=false # Database settings # IMPORTANT: Set a strong POSTGRES_PASSWORD in production (required by docker-compose). # Never reuse the example password below outside local throwaway environments. DATABASE_URL=postgresql+psycopg2://timetracker:CHANGE_ME_STRONG_PASSWORD@db:5432/timetracker POSTGRES_DB=timetracker POSTGRES_USER=timetracker POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD POSTGRES_HOST=db # SQLAlchemy connection pool (PostgreSQL). Defaults suit a single app process. # Raise for multi-worker / high concurrency; keep pool_size * workers under DB max_connections. # DB_POOL_SIZE=5 # DB_MAX_OVERFLOW=10 # PostgreSQL statement/lock timeouts (milliseconds, PostgreSQL-only). # Applied only under the eventlet gunicorn worker (not CLI / flask db upgrade). # Bound how long a query may run or wait for a lock before the connection is # aborted — prevents one stuck query from wedging the whole app process. # idle_in_transaction defaults to 0 (disabled) so scheduler jobs that hold a # session open across HTTP calls are not killed. 0 disables a limit. DB_STATEMENT_TIMEOUT_MS=30000 DB_LOCK_TIMEOUT_MS=10000 DB_IDLE_TX_TIMEOUT_MS=0 # Docker Compose: nginx host port mappings (used by docker-compose.yml) # Override when 80/443 are in use (e.g. behind Nginx Proxy Manager, Traefik, Caddy). # Example: HTTP_PORT=8180 HTTPS_PORT=8443 # HTTP_PORT=80 # HTTPS_PORT=443 # Session settings # Defaults: secure cookies true unless FLASK_ENV=development/dev or FLASK_DEBUG=true. # ProductionConfig also defaults these to true. Set false only for HTTP/local access. SESSION_COOKIE_SECURE=false SESSION_COOKIE_HTTPONLY=true PERMANENT_SESSION_LIFETIME=86400 # REMEMBER_COOKIE_SECURE= # inherits same secure-by-default logic when unset # Rate limiting storage (use Redis in multi-worker production; memory:// is not shared) # RATELIMIT_STORAGE_URI=memory:// # RATELIMIT_STORAGE_URI=redis://:${REDIS_PASSWORD}@redis:6379/1 # Optional Redis (docker compose --profile redis up -d) # REDIS_PASSWORD is required by the redis compose profile (no default). # REDIS_ENABLED=true # REDIS_PASSWORD=generate-a-long-random-secret # REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0 # Web Push (browser "Still working?" notifications, works with the browser closed). # Generate a VAPID keypair once: npx web-push generate-vapid-keys # Without these, browser push is skipped and only the in-page prompt is shown. # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= # VAPID_CONTACT_EMAIL=admin@example.com # Protect Prometheus /metrics (header X-Metrics-Token or ?token=). Required for safe production scrapes. # METRICS_TOKEN=generate-a-long-random-secret # Monitoring profile (Grafana): set GRAFANA_ADMIN_PASSWORD — UI binds to 127.0.0.1 only. # GRAFANA_ADMIN_PASSWORD=generate-a-long-random-secret # CORS / Socket.IO: comma-separated browser origins allowed for cross-origin /api/v1 calls. # Same-origin always works. Without this, only APP_BASE_URL / SERVER_NAME origins are allowed. # CORS_ALLOWED_ORIGINS=https://time.example.com,https://app.example.com # CalDAV / ActivityWatch may target LAN hosts when true (default). Cloud metadata hosts stay blocked. # INTEGRATIONS_ALLOW_PRIVATE_URLS=true # Application settings TZ=Europe/Rome CURRENCY=EUR # Public base URL of this instance (no trailing path), e.g. https://time.example.com # Required for correct absolute links in emails sent by background jobs (hour-limit # notices, weekly summaries, etc.). If unset, the app learns the host from live # browser requests; set explicitly for reliable links right after deploy. # APP_BASE_URL=https://time.example.com # Preferred URL scheme used when building absolute URLs from SERVER_NAME (if set). PREFERRED_URL_SCHEME=https ROUNDING_MINUTES=1 ROUNDING_METHOD=nearest ROUNDING_MINIMUM_MINUTES=0 ROUNDING_ENFORCE_GLOBAL=false # Seeds Settings.single_active_timer on first settings row; admin System Settings overrides thereafter. SINGLE_ACTIVE_TIMER=true IDLE_TIMEOUT_MINUTES=30 # What happens when "Still working?" is unanswered: review (default) or auto_stop IDLE_UNANSWERED_ACTION=review # Global Flask-Limiter defaults (semicolon or comma separated). # Defaults to "5000 per day;1000 per hour" when unset; keyed per logged-in user (else IP). # RATELIMIT_DEFAULT=5000 per day;1000 per hour # Gunicorn worker recycle (docker/start-fixed.py → /app/start.py). # When PORT is set (Render/Railway/etc.), max-requests defaults to 0 (disabled) so a # single eventlet worker does not recycle into connection-refused health blips. # When PORT is unset (local docker-compose), default is 1000. Explicit values always win. # GUNICORN_MAX_REQUESTS=0 # GUNICORN_MAX_REQUESTS_JITTER=100 # Optional: Firebase Cloud Messaging for mobile idle "Still working?" wake-up (Issue #722). # Path to a service-account JSON file, or the raw JSON string. # FIREBASE_SERVICE_ACCOUNT_JSON=/run/secrets/firebase-service-account.json # Smart in-app notifications (session toasts; see docs/features/SMART_NOTIFICATIONS.md) # SMART_NOTIFY_MAX_PER_DAY=2 # SMART_NOTIFY_NO_TRACKING_AFTER=16:00 # SMART_NOTIFY_SUMMARY_AT=18:00 # SMART_NOTIFY_LONG_TIMER_HOURS=4 # SMART_NOTIFY_SCHEDULER_SLOT_MINUTES=30 # Background scheduler (APScheduler). Default true. Set false on web-only replicas # when a dedicated worker runs jobs. With Postgres, only one process acquires a # session-level advisory lock (held on a dedicated DB connection for the process # lifetime) and starts BackgroundScheduler; other processes skip starting it. # SCHEDULER_ENABLED=true # AI helper (server-side; API keys are never sent to clients) # Default install has no bundled LLM. Leave AI_ENABLED=false unless you use an external API or enable Ollama. AI_ENABLED=false # --- Optional: bundled Ollama (same machine as Docker Compose) --- # Start the stack with: docker compose --profile ai up -d # Then set: # AI_ENABLED=true # AI_PROVIDER=ollama # AI_BASE_URL=http://ollama:11434 # AI_MODEL=llama3.1 # OLLAMA_KEEP_ALIVE=5m # how long Ollama keeps the model resident in memory # --- Optional: hosted OpenAI-compatible API only (no Ollama profile) --- # AI_ENABLED=true # AI_PROVIDER=openai_compatible # AI_BASE_URL=https://api.example.com # AI_MODEL=... # AI_API_KEY=... # --- Optional: OrcaRouter (hosted; signup: https://www.orcarouter.ai/ref/ref_984e584cb4cd86485a8c) --- # AI_ENABLED=true # AI_PROVIDER=orcarouter # AI_BASE_URL=https://api.orcarouter.ai # AI_MODEL= # AI_API_KEY= AI_PROVIDER=ollama AI_BASE_URL=http://ollama:11434 AI_MODEL=llama3.1 # AI_API_KEY= # only needed when AI_PROVIDER=openai_compatible AI_TIMEOUT_SECONDS=60 AI_CONTEXT_LIMIT=40 # SCIM 2.0 provisioning (Users CRUD at /scim/v2 when enabled) # SCIM_ENABLED=false # SCIM_BEARER_TOKEN=generate-a-long-random-secret # API token rate limits (per token; Redis recommended for multi-worker) # API_TOKEN_RATE_LIMIT_PER_MINUTE=100 # API_TOKEN_RATE_LIMIT_PER_HOUR=1000 # User management ALLOW_SELF_REGISTER=true # Comma-separated admin usernames. Only the first username is automatically created during database initialization. # The default admin has no password—set it on first login (enter username + any password 8+ chars). # Additional admin usernames must either self-register (if ALLOW_SELF_REGISTER=true) or be created manually. # Example: ADMIN_USERNAMES=admin,manager - only "admin" is created automatically; "manager" must self-register or be created manually. ADMIN_USERNAMES=admin # Authentication # Options: none | local | oidc | ldap | saml | both | all # none = No password authentication (username only) # local = Password authentication required # oidc = OIDC/Single Sign-On only # ldap = LDAP bind only # saml = SAML 2.0 SSO only # both = OIDC + local password (backwards compatible) # all = local + OIDC + LDAP + SAML AUTH_METHOD=local # SAML 2.0 SSO (used when AUTH_METHOD=saml or all, or SAML_ENABLED=true) # SAML_ENABLED=false # SAML_SP_ENTITY_ID=https://yourapp.example.com/auth/saml/metadata # SAML_SP_ACS_URL=https://yourapp.example.com/auth/saml/acs # SAML_SP_X509_CERT= # SAML_SP_PRIVATE_KEY= # SAML_IDP_ENTITY_ID=https://idp.example.com/metadata # SAML_IDP_SSO_URL=https://idp.example.com/sso # SAML_IDP_X509_CERT= # SAML_IDP_METADATA_URL= # SAML_WANT_ASSERTIONS_SIGNED=true # SAML_CLOCK_SKEW_SECONDS=120 # SAML_ADMIN_GROUP= # SAML_ADMIN_EMAILS= # Security hardening (recommended) # Settings secrets encryption-at-rest (mail passwords, OAuth secrets, Peppol token, AI key, 2FA secret). # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # SETTINGS_ENCRYPTION_KEY= # Or mount from file: # SETTINGS_ENCRYPTION_KEY_FILE=/run/secrets/timetracker_settings_key # # Password reset link lifetime (seconds) # PASSWORD_RESET_TOKEN_MAX_AGE_SECONDS=3600 # # Require TOTP 2FA for admin accounts (admins will be prompted to enroll) # REQUIRE_2FA_FOR_ADMINS=false # LDAP Authentication (used when AUTH_METHOD=ldap or all) # LDAP_HOST=ldap.example.com # LDAP_PORT=389 # LDAP_USE_SSL=false # LDAP_USE_TLS=false # LDAP_BIND_DN=cn=serviceaccount,dc=example,dc=com # LDAP_BIND_PASSWORD=secret # LDAP_BASE_DN=dc=example,dc=com # LDAP_USER_DN=ou=users # LDAP_USER_OBJECT_CLASS=inetOrgPerson # LDAP_USER_LOGIN_ATTR=uid # LDAP_USER_EMAIL_ATTR=mail # LDAP_USER_FNAME_ATTR=givenName # LDAP_USER_LNAME_ATTR=sn # LDAP_GROUP_DN=ou=groups # LDAP_GROUP_OBJECT_CLASS=groupOfNames # LDAP_ADMIN_GROUP=timetracker-admins # LDAP_REQUIRED_GROUP=timetracker-users # LDAP_TLS_CA_CERT_FILE=/certs/ldap-ca.crt # LDAP_TIMEOUT=10 # OIDC (used when AUTH_METHOD=oidc, both, or all) # OIDC_ISSUER=https://login.microsoftonline.com//v2.0 # OIDC_CLIENT_ID= # OIDC_CLIENT_SECRET= # OIDC_REDIRECT_URI=https://yourapp.example.com/auth/oidc/callback # OIDC_SCOPES=openid profile email # OIDC_USERNAME_CLAIM=preferred_username # OIDC_FULL_NAME_CLAIM=name # OIDC_EMAIL_CLAIM=email # OIDC_GROUPS_CLAIM=groups # OIDC_ADMIN_GROUP= # OIDC_ADMIN_EMAILS= # Optional: RP-Initiated Logout. Only set if your provider supports end_session_endpoint. # If unset, logout will be local only (recommended for providers like Authelia). # If set, TimeTracker will redirect to the provider's logout endpoint. # OIDC_POST_LOGOUT_REDIRECT_URI=https://yourapp.example.com/ # Backup settings (scheduler creates a full archive daily at backup_time and prunes older ones) BACKUP_RETENTION_DAYS=30 BACKUP_TIME=02:00 # Optional: override where backup archives are stored (default: /backups) # BACKUP_FOLDER=/data/uploads/backups # Email settings (Flask-Mail) # Configure these to enable email notifications and features # MAIL_SERVER=smtp.gmail.com # MAIL_PORT=587 # MAIL_USE_TLS=true # MAIL_USE_SSL=false # MAIL_USERNAME=your-email@gmail.com # MAIL_PASSWORD=your-app-password # MAIL_DEFAULT_SENDER=noreply@yourdomain.com # MAIL_MAX_EMAILS=100 # Peppol e-invoicing (optional) # Requires an access point provider. This app uses a generic HTTP adapter. # Enable: # PEPPOL_ENABLED=true # # Sender identifiers (your company in Peppol) # Example endpoint scheme ids: 0088 (GLN), 9906 (VAT), 0208 (LEI) depending on your country/AP rules # PEPPOL_SENDER_ENDPOINT_ID= # PEPPOL_SENDER_SCHEME_ID= # Optional: # PEPPOL_SENDER_COUNTRY=BE # # Access point adapter (HTTP JSON endpoint your AP exposes) # PEPPOL_ACCESS_POINT_URL= # PEPPOL_ACCESS_POINT_TOKEN= # PEPPOL_ACCESS_POINT_TIMEOUT=30 # PEPPOL_PROVIDER=generic # File upload settings MAX_CONTENT_LENGTH=16777216 UPLOAD_FOLDER=/data/uploads # CSRF protection # IMPORTANT: Keep CSRF enabled in production for security # Only disable for development/testing if needed WTF_CSRF_ENABLED=true WTF_CSRF_TIME_LIMIT=3600 # CSRF SSL Strict Mode # Set to false if accessing via HTTP (localhost or IP address) # Set to true only when using HTTPS in production WTF_CSRF_SSL_STRICT=false # CSRF Cookie Settings # Only set these if you need to access the app via IP address or have cookie issues # CSRF_COOKIE_SECURE=false # Set to false for HTTP access # CSRF_COOKIE_SAMESITE=Lax # Options: Strict, Lax, None # CSRF_COOKIE_DOMAIN= # Leave empty for single domain, set for subdomains # Session Cookie Settings for IP Address Access # If accessing via IP address (e.g., 192.168.1.100), use these settings: # SESSION_COOKIE_SAMESITE=Lax # Change to 'None' only if needed for cross-site # SESSION_COOKIE_SECURE=false # Must be false for HTTP # TROUBLESHOOTING CSRF issues ("CSRF token missing or invalid" errors): # 1. SECRET_KEY changed? All CSRF tokens become invalid when SECRET_KEY changes # 2. Cookies blocked? Check browser settings and allow cookies from your domain # 3. Behind a proxy? Ensure proxy forwards cookies and doesn't strip them # 4. Token expired? Increase WTF_CSRF_TIME_LIMIT (in seconds) # 5. Multiple app instances? All must use the SAME SECRET_KEY # 6. Clock skew? Ensure server time is synchronized (use NTP) # 7. Accessing via IP? Set WTF_CSRF_SSL_STRICT=false and SESSION_COOKIE_SECURE=false # 8. Still broken? Try: docker-compose restart app # 9. For testing only: Set WTF_CSRF_ENABLED=false (NOT for production!) # See docs/CSRF_CONFIGURATION.md for detailed troubleshooting # Logging LOG_LEVEL=INFO LOG_FILE=/data/logs/timetracker.log # Analytics and Monitoring # All analytics features are optional and disabled by default # Sentry Error Monitoring (optional) # Get your DSN from https://sentry.io/settings/projects/ # SENTRY_DSN= # SENTRY_TRACES_RATE=0.0 # OTEL OTLP export (optional sink) # Example endpoint: https://otlp-gateway-prod-eu-west-2.grafana.net/otlp # OTEL_EXPORTER_OTLP_ENDPOINT= # OTEL_EXPORTER_OTLP_TOKEN= # # OpenTelemetry SDK (traces + metrics to OTLP; uses same endpoint/token as above) # When unset, tracing and metrics default to enabled if OTLP credentials are present. # Server-side export to the shared (baked-in) Grafana Cloud tenant also requires the # telemetry opt-in below (ENABLE_TELEMETRY / admin dashboard). Setting your own # OTEL_EXPORTER_OTLP_* env vars allows export without that opt-in (you own the backend). # ENABLE_TRACING=true # ENABLE_METRICS=true # Optional: OTLP metrics export interval in milliseconds (default 60000) # OTEL_METRICS_EXPORT_INTERVAL_MS=60000 # # Tests only: in-memory tracing without network (pytest) # OTEL_ENABLE_IN_TESTS=1 # Telemetry (optional, opt-in, anonymous) # Sends anonymous installation data to Grafana OTLP (version/platform/install heartbeat) # Also gates server-side OTel traces/metrics when using baked-in OTLP credentials. # Requires OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TOKEN (or baked-in defaults) # Default: false (disabled) # See docs/privacy.md for details # ENABLE_TELEMETRY=true # TELE_SALT=8f4a7b2e9c1d6f3a5e8b4c7d2a9f6e3b1c8d5a7f2e9b4c6d3a8f5e1b7c4d9a2f # APP_VERSION= # Automatically read from setup.py, override only if needed