# --------------------------------------------------------------------------- # Required deployment settings # --------------------------------------------------------------------------- # Use a long, random value in production. Changing this invalidates signed # tokens and sessions. Production refuses to start with the built-in dev key. SECRET_KEY=change-me-to-a-long-random-secret-at-least-32-chars # DEBUG controls developer behavior: verbose error pages, permissive local CORS # defaults, and admin UI defaulting. Keep false for production. DEBUG=false # Public URLs where users normally reach the app. The backend derives allowed # CSRF/CORS origins and default allowed hosts from these URLs. Use https URLs in # production. # # URL layout: the marketing landing page is served at the root "/", and the # actual app (PWA) is served under "/app" (e.g. https://splex.example.com/app). # FRONTEND_PUBLIC_URL is the bare public origin (the landing root); user-facing # app links (magic login, invites, emails) are built as FRONTEND_PUBLIC_URL/app. # The JSON API stays at "/api". See SERVE_LANDING below to disable the landing. FRONTEND_PUBLIC_URL=https://splex.example.com BACKEND_PUBLIC_URL=https://splex.example.com # Is the Django container behind a reverse proxy such as Caddy, Traefik, Nginx, # or a cloud load balancer? APP_BEHIND_PROXY=true # Number of reverse proxies in front of the app. Rate limiting reads the real # client IP from a fixed position in X-Forwarded-For using this count; set it # correctly or an attacker can spoof the header to bypass every IP throttle. # 0 = directly exposed (use the socket peer, ignore X-Forwarded-For) # 1 = behind one reverse proxy (nginx/Traefik/Caddy/...) # 2 = e.g. Cloudflare in front of your own reverse proxy # Leave unset to default to 1 when APP_BEHIND_PROXY=true, else 0. NUM_PROXIES=1 # Does that proxy expose the public service via TLS/HTTPS? # If true, Django enables HTTPS redirects, secure cookies, HSTS, and trusts the # proxy's X-Forwarded-Proto header. PROXY_USES_TLS=true # Serve the built PWA (the app) from the same Django container, under /app. SERVE_PWA=true # Serve the static marketing landing page at the root "/". When false, "/" # redirects to the app at /app instead (useful for private/self-hosted-only # instances that don't want a public landing page). SERVE_LANDING=false # Enable Django admin UI route (/admin). Keep disabled in production unless # explicitly needed. ENABLE_ADMIN_UI=false # Allow new user accounts to be created at login. When set to false, only # existing users may sign in - magic link and Google login both reject unknown # email addresses. Useful for closed self-hosted instances where you only want # yourself and people you invited to be able to register. ALLOW_REGISTRATION=true # Data retention: accounts that have not logged in for this many months are # automatically deleted after two warning emails. The first notice is sent # ~14 days before deletion and the second ~7 days before. # The same window also governs the permanent purge of soft-deleted records # (deleted groups, ended friendships, removed expenses/settlements/memberships): # once soft-deleted for longer than this, they are removed from the database # for good. Set to 0 to disable both automatic deletion and the purge. # Splex enforces this automatically every 24 hours in the background. DATA_RETENTION_INACTIVE_MONTHS=6 # Receipts: per-file size limit (in bytes) for receipt uploads (default 15 MiB). # Set to 0 to disable the per-file limit. RECEIPT_MAX_FILE_SIZE_BYTES=15728640 # Receipts: maximum combined size (in bytes) of all receipts within a single # group. New uploads are rejected once a group exceeds this quota. # Default 100 MiB. Friendship-context receipts are not subject to this quota. # Set to 0 to disable the per-group quota. RECEIPT_MAX_GROUP_TOTAL_BYTES=104857600 # Receipts: how long a draft receipt (uploaded but never attached to an # expense) is kept before the cleanup job removes it. Default 24 hours. # Set to 0 to disable the cleanup job entirely (drafts kept indefinitely). RECEIPT_DRAFT_RETENTION_HOURS=24 # Default backend URL offered by the Android app login settings. The web app # always uses the domain it is served from. EXPO_PUBLIC_DEFAULT_API_BASE_URL=https://splex.example.com # --------------------------------------------------------------------------- # Email / magic login # --------------------------------------------------------------------------- # SMTP is required for production magic links and login codes. EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend EMAIL_HOST=smtp.example.com EMAIL_PORT=587 EMAIL_USE_TLS=true EMAIL_HOST_USER= EMAIL_HOST_PASSWORD= DEFAULT_FROM_EMAIL=Splex # Development alternative: print emails to stdout instead of sending them. # Magic login links and codes appear directly in `docker compose logs app`. # Replace the EMAIL_BACKEND line above with this and remove the SMTP settings. # EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend # --------------------------------------------------------------------------- # Push notifications # --------------------------------------------------------------------------- # Android push notifications use Expo Push tokens. # Web Push uses VAPID to identify this application to browser push services. # Leave VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY empty to auto-generate a # long-lived key pair in the database. Set them only when you want to provide # and rotate keys manually. VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= # VAPID_SUBJECT is a contact URI for browser push services. Use a mailto: # address or HTTPS contact page controlled by you. VAPID_SUBJECT=mailto:noreply@example.com # Push tokens not re-registered (the app re-registers on every launch) and not # successfully delivered to for this many days are deleted. A returning device # transparently re-registers on its next launch. 0 keeps tokens forever. PUSH_TOKEN_TTL_DAYS=365 # --------------------------------------------------------------------------- # Optional configuration # --------------------------------------------------------------------------- # ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, and CORS_ALLOWED_ORIGINS are derived from # FRONTEND_PUBLIC_URL and BACKEND_PUBLIC_URL. Override only for unusual setups. # ALLOWED_HOSTS=splex.example.com # CSRF_TRUSTED_ORIGINS=https://splex.example.com # CORS_ALLOWED_ORIGINS=https://splex.example.com # Default self-hosted setup uses SQLite in the persistent Docker volume. SQLITE_PATH=/app/data/splex.sqlite3 # Optional, when using an external PostgreSQL database: # DATABASE_URL=postgres://splex:splex@postgres:5432/splex # Access tokens are intentionally short-lived. Refresh tokens are long-lived # and are refreshed automatically by the frontend. ACCESS_TOKEN_LIFETIME_MINUTES=15 REFRESH_TOKEN_LIFETIME_DAYS=180 # Public auth endpoints are throttled by IP/user scope. Rates use DRF format. THROTTLE_ANON_RATE=120/minute THROTTLE_USER_RATE=600/minute THROTTLE_MAGIC_LINK_RATE=5/minute THROTTLE_MAGIC_CODE_RATE=20/minute THROTTLE_MAGIC_TOKEN_RATE=20/minute THROTTLE_INVITATION_PREVIEW_RATE=60/minute THROTTLE_PRIVATE_MEDIA_RATE=120/minute # Anti-abuse limits for passwordless login (in addition to the IP throttles # above). MAGIC_LINK_MAX_EMAILS_PER_DAY caps how many magic-login emails may be # sent to one recipient address per rolling 24h, across all source IPs, to stop # email-bombing / relay abuse (0 disables). MAGIC_CODE_MAX_ATTEMPTS is how many # wrong 6-digit codes a single challenge tolerates before it is invalidated, to # stop distributed brute force of the code (0 disables). MAGIC_LINK_MAX_EMAILS_PER_DAY=10 MAGIC_CODE_MAX_ATTEMPTS=5 # Currency conversion. Frankfurter currently does not require an API key. CURRENCY_RATE_PROVIDER=frankfurter CURRENCY_RATE_API_BASE_URL=https://api.frankfurter.dev CURRENCY_RATE_API_KEY= # Gunicorn runtime settings. Threaded workers prevent one idle client # connection from blocking the single-container app. GUNICORN_WORKERS=2 GUNICORN_THREADS=4 GUNICORN_TIMEOUT=60 # Legal document source files. The backend serves each one publicly at # /api/tos/, /api/privacy/ and /api/imprint/, and the frontend exposes them at # /tos, /privacy and /imprint. The container creates a styled placeholder at # each path automatically if the file does not exist yet. TOS_FILE_PATH=/app/data/tos.html PRIVACY_FILE_PATH=/app/data/privacy.html IMPRINT_FILE_PATH=/app/data/imprint.html # --------------------------------------------------------------------------- # Google login (optional) # --------------------------------------------------------------------------- # Allows users to sign in with their Google account in addition to magic links. # Leave both variables empty (the default) to disable Google login entirely. # # To support both the PWA and the Android app you need TWO OAuth client IDs in # Google Cloud Console - they are separate credential types: # - a "Web application" client → for the PWA → GOOGLE_CLIENT_ID # - an "Android" client → for the native Android UX → GOOGLE_ANDROID_CLIENT_ID # The Android client is optional. If you skip it, Google login still works in the # Android app via a browser redirect, just without the native account picker. # # Setup steps: # 1. Open https://console.cloud.google.com/ and select (or create) your project. # 2. Go to "APIs & Services" → "OAuth consent screen" and configure it. # 3. Go to "APIs & Services" → "Credentials" → "Create credentials" # → "OAuth client ID". # # 4. Create the "Web application" client (always needed for the PWA): # - Authorized JavaScript origins: your frontend URL, no trailing slash # (e.g. https://splex.example.com) # - Authorized redirect URIs: your frontend URL + /app/login # (e.g. https://splex.example.com/app/login) # Note: the app lives under /app and Google's match is exact, so the # redirect URI must be exactly the app login URL (origin + /app/login). # That is what the web client sends to Google. If Google still returns # "redirect_uri_mismatch", open the error page - it shows the exact URI # that was sent - and paste that verbatim into the authorized list. # Copy the resulting Client ID into GOOGLE_CLIENT_ID below. # # 5. Create the "Android" client (only if you want native Google sign-in in the # Android app): # # a) If you are using the official Play Store build of Splex: # - Package name: com.sterul.splex # - SHA-1 fingerprint: D9:AE:69:C5:6E:D1:DB:78:0E:CC:37:A7:3C:16:C3:9E:06:9D:A3:14 # b) If you are using APK from github: # - Package name: com.sterul.splex # - SHA-1 fingerprint: FA:FA:0E:21:40:1F:27:72:85:F6:EB:A6:D4:E0:42:27:99:35:51:5D # # c) If you are building the Android app yourself from source: # - Package name: your own application id # - SHA-1 fingerprint: your release/debug keystore SHA-1 # (find it with `eas credentials` or `keytool -list -v -keystore ...`) # # Copy the resulting Client ID into GOOGLE_ANDROID_CLIENT_ID below. GOOGLE_CLIENT_ID= GOOGLE_ANDROID_CLIENT_ID= # --------------------------------------------------------------------------- # Demo mode (optional) # --------------------------------------------------------------------------- # When enabled, the login screen shows a "Try demo" button that runs the app # against in-memory mock data on the device. No data is sent to the backend, # and write actions are blocked with a snackbar. Useful for public hosted # instances that want to let visitors explore the app without signing up. DEMO_MODE_ENABLED=false # --------------------------------------------------------------------------- # Risky imports (optional) # --------------------------------------------------------------------------- # Enables import sources that open outbound network connections from the # Splex server to a user-supplied endpoint. Currently this gates the Split Pro # database import, which connects to an arbitrary PostgreSQL host on behalf # of the user. Two risks to be aware of before enabling: # - the endpoint is reachable from the Splex container's network, so any # internal host the container can reach is also reachable through this # import (e.g. databases or admin panels behind your firewall). # - if the target is a malicious server speaking the PostgreSQL wire # protocol, weaknesses in the client library could in theory be exploited # via crafted responses. # Leave disabled unless you actually need it. ENABLE_RISKY_IMPORTS=false