Configuration Reference

All configuration is done through environment variables on the server service. The web app is a static SPA that only needs VITE_SERVER_URL at build time.

Required variables

VariableDescription
DATABASE_URLPostgreSQL connection string. When using built-in Postgres: postgresql://bittery:${DB_PASSWORD}@postgres:5432/bittery
JWT_SECRETSecret key for signing authentication tokens. Generate with openssl rand -hex 32
DOMAINYour domain name (used by Caddy for TLS)

Danger

Never reuse JWT_SECRET across instances or commit it to version control. Rotating this secret will invalidate all active sessions.

Server settings

VariableDefaultDescription
BITTERY_MODEself-hostedOperating mode. Use self-hosted for self-hosted deployments
BITTERY_CLOUD_PUBLIC_SIGNUPtrueCloud-only flag. Set false on hosted Bittery Cloud to make signup invite-only
BITTERY_CLOUD_BILLING_ENABLEDtrueCloud-only flag. Set false on hosted Bittery Cloud to disable Stripe billing
CORS_ORIGINhttps://${DOMAIN}Comma-separated list of allowed origins for API requests
TRUST_PROXY_MODEnoneHow to read client IPs. Options: none, cloudflare, forwarded. See the warning under Rate limiting
PORT3000Port the API server listens on
MIGRATIONS_FOLDER/app/migrationsPath to SQL migration files

Rate limiting

The per-IP limits key on the client address resolved via TRUST_PROXY_MODE. With the default none, that is the TCP peer address, which a caller cannot forge.

Only set cloudflare or forwarded if the server is genuinely behind a proxy that overwrites CF-Connecting-IP / X-Forwarded-For on every request. If the server is also reachable directly, those headers are attacker-controlled and rotating them hands out a fresh per-IP budget per request, defeating the limits below. Conversely, leaving it at none behind a proxy makes every request appear to come from the proxy, collapsing all clients onto one shared budget.

VariableDefaultDescription
RATE_LIMIT_ADAPTERautoRate limiter backend: auto, postgres, or redis. auto uses Redis when RATE_LIMIT_REDIS_URL is set, otherwise Postgres. redis requires RATE_LIMIT_REDIS_URL (fails at startup if unset). There is no in-memory backend
RATE_LIMIT_REDIS_URLRedis/Valkey URL for the Redis rate-limiting backend
SHARE_LINK_DAILY_LIMIT50Maximum shared links a user can create per fixed 24-hour window

Tuning limits

Override the maximum request counts below. The time windows are fixed in code; only the counts (and the recovery lockout duration) are configurable.

VariableDefaultDescription
RATE_LIMIT_LOGIN_IP20Login attempts per 15 minutes per IP
RATE_LIMIT_LOGIN_EMAIL10Login attempts per 15 minutes per email
RATE_LIMIT_SIGNUP_IP10Signups per hour per IP
RATE_LIMIT_SIGNUP_EMAIL5Signups per hour per email
RATE_LIMIT_RECOVERY_REQUEST5Recovery requests per hour
RATE_LIMIT_RECOVERY_VERIFY_MAX5Recovery verification attempts before lockout
RATE_LIMIT_RECOVERY_LOCK_MINUTES15Recovery lockout duration in minutes
RATE_LIMIT_AUTH_IP30Requests per 15 minutes per IP for other auth endpoints

Object storage (S3-compatible)

Required only if you enable the storage profile for file attachments.

VariableDefaultDescription
BITTERY_STORAGE_ENDPOINTS3 endpoint URL (e.g. http://minio:9000)
BITTERY_STORAGE_BUCKETS3 bucket name
BITTERY_STORAGE_ACCESS_KEY_IDS3 access key
BITTERY_STORAGE_SECRET_ACCESS_KEYS3 secret key
BITTERY_STORAGE_REGIONautoS3 region
BITTERY_STORAGE_CDN_URLPublic CDN URL for uploaded files

Cache (Redis / Valkey)

VariableDefaultDescription
REDIS_URLRedis/Valkey connection URL for pub/sub and caching

Built-in PostgreSQL

These apply when using the builtin-db profile:

VariableDescription
DB_PASSWORDPassword for the built-in PostgreSQL instance
POSTGRES_DBDatabase name (default: bittery)
POSTGRES_USERDatabase user (default: bittery)

Web app (build-time only)

VariableDescription
VITE_SERVER_URLAPI server URL. Inlined at build time by Vite
VITE_BILLING_MARKETING_ENABLEDMarketing-site flag for hosted pricing and billing documentation. Defaults to disabled

Note

Since VITE_SERVER_URL is inlined at build time, changing it requires rebuilding the web image. For Docker Compose deployments with Caddy, the web app and server share the same domain, so this is not needed.