# Super Sync Server - Production Environment Configuration # Copy this file to .env and configure all required values # ============================================================================= # REQUIRED SETTINGS # ============================================================================= # Your domain (without https://) # Caddy will automatically obtain SSL certificates for this domain DOMAIN=sync.your-domain.com # JWT secret for authentication tokens (minimum 32 characters) # Generate with: openssl rand -base64 32 JWT_SECRET=your-secure-jwt-secret-minimum-32-characters # PostgreSQL password for the database # Generate with: openssl rand -base64 24 POSTGRES_PASSWORD=your-secure-postgres-password # ============================================================================= # SERVER SETTINGS (optional - defaults provided) # ============================================================================= # Server bind address (default: 0.0.0.0) # Use :: for IPv6-only deployments. HOST=0.0.0.0 # ============================================================================= # DATABASE SETTINGS (optional - defaults provided) # ============================================================================= # PostgreSQL user (default: supersync) # POSTGRES_USER=supersync # PostgreSQL database name (default: supersync) # POSTGRES_DB=supersync # External PostgreSQL connection string. Leave unset to use the bundled compose # postgres service. The bundled service is reachable as postgres:5432; db:5432 # is also kept as a compatibility alias for older .env files. If you point this # at an external host, also set POSTGRES_SERVICE= below. # # IMPORTANT: append ?connection_limit=N&pool_timeout=10 to bound Prisma's pool. # Prisma's DEFAULT pool size is ~(host_cpu_count * 2), read from HOST cores — # NOT the container cpu limit. On a many-core host an unbounded pool silently # consumes the entire postgres max_connections cap (compose: 120), leaving zero # headroom for psql, migrations, the old-ops cleanup job, or the reconnect # stampede after a crash recovery -> "sorry, too many clients already" and a # fleet-wide WS reconnect storm. Keep N well below max_connections (e.g. 60). # # Setting DATABASE_URL here REPLACES the compose default wholesale, so these # params are not inherited — they must be carried across explicitly. # DATABASE_URL=postgresql://supersync:password@example.com:5432/supersync?connection_limit=60&pool_timeout=10 # # OPTIONAL RECOVERY GUARDRAIL — enable only after checking legacy histories: # ...&options=-c%20statement_timeout%3D60000 # Without it a single degenerate query plan holds a pool connection until someone # kills it by hand: 75 minutes in the 2026-07-20 outage, which consumed the whole # pool and failed every user's sync, including requests unrelated to the bad # query. Prisma's transaction timeout only aborts the client side; the backend # keeps running. This is what makes pool exhaustion recoverable rather than a # state the server cannot exit on its own, and it was absent in all three # operations-table incidents. Only the connection-string form works — Prisma # overrides both ALTER ROLE and ALTER DATABASE statement_timeout. Keep the # shown %20 (space) and %3D (=) encoding for portability across URL tooling. # # NOT enabled by default because one legacy path legitimately exceeds 60s: # `_computeSnapshotVectorClock` (src/sync/services/operation-download.service.ts) # aggregates the vector clock across a user's whole history, and was moved out of # its transaction because large histories trip the 60s transaction budget. It # runs when persisted snapshot-clock metadata is missing, stale, or invalid. On # upgraded instances with long histories, a 60s cap can turn those downloads into # deterministic 500s. Either confirm that fallback no longer fires for your users, # or set a budget above its worst case. (#9191) # # scripts/migrate-deploy.sh replaces this cap for the migrator with a finite # statement timeout five seconds below MIGRATE_STEP_TIMEOUT, allowing long # CREATE INDEX CONCURRENTLY work without leaving an unbounded backend query. # ============================================================================= # CORS SETTINGS # ============================================================================= # CORS allowed origins (comma-separated) # Default: https://app.super-productivity.com # Add your self-hosted frontend URL if applicable CORS_ORIGINS=https://app.super-productivity.com # ============================================================================= # SMTP CONFIGURATION (Required for email verification) # ============================================================================= # SMTP server hostname SMTP_HOST=smtp.your-email-provider.com # SMTP port (default: 587 for TLS, 465 for SSL) SMTP_PORT=587 # Use SSL/TLS (true for port 465, false for STARTTLS on 587) SMTP_SECURE=false # SMTP authentication credentials SMTP_USER=your-smtp-username SMTP_PASS=your-smtp-password # From address for emails SMTP_FROM="Super Productivity Sync" # ============================================================================= # CONTAINER REGISTRY (for build-and-push.sh) # ============================================================================= # GitHub username for GHCR # GHCR_USER=your-github-username # GitHub token with "Packages: Read and write" permission # Create at: https://github.com/settings/tokens?type=beta # GHCR_TOKEN=your-github-token # ============================================================================= # ADVANCED SETTINGS (usually no changes needed) # ============================================================================= # Log level (info, debug, warn, error) # LOG_LEVEL=info # Startup migrations are disabled by default. Use scripts/deploy.sh for # production and local Docker deploys so migrations run once before the app # container is replaced. Only set this to true for one-off/manual runs. # RUN_MIGRATIONS_ON_STARTUP=false # Compose service used for database readiness checks. Leave unset to use the # bundled postgres service. Set POSTGRES_SERVICE= to an empty value only when # DATABASE_URL points to an external database; deploy.sh will then start # supersync/caddy without compose dependencies. Prisma migrations still run # against the configured DATABASE_URL. # POSTGRES_SERVICE= # Old operation retention cleanup deletes in bounded batches so a backlog cannot # monopolize PostgreSQL. Increase carefully on larger servers. # OLD_OPS_CLEANUP_DELETE_BATCH_SIZE=5000 # OLD_OPS_CLEANUP_MAX_DELETED_PER_RUN=25000 # Timeout in seconds for `prisma migrate deploy` during ./scripts/deploy.sh. # Online index builds (CREATE INDEX CONCURRENTLY) can block on long-running # transactions; raise this for very large tables (3600 is a safe starting point # for a large operations table — the 900 default can SIGTERM a slow index build # part-way through). A timed-out migrator container is now force-removed, so a # too-low value no longer wedges the NEXT deploy with a leaked advisory lock # (P1002); the deploy fails loudly with copy-paste recovery steps instead. Note # an interrupted CREATE INDEX CONCURRENTLY can still need a one-time INVALID-index # drop (the failure output prints the exact steps) before the re-run succeeds. # Exit code 124 = timed out. # MIGRATION_TIMEOUT=900 # Per-migration-step timeout (seconds) enforced inside the image. deploy.sh # derives it from MIGRATION_TIMEOUT (minus a small margin) and forwards it, so # raising MIGRATION_TIMEOUT is enough for the deploy.sh path. Set this directly # only for the image CMD / helm initContainer paths, which have no outer host # timeout. Defaults to 1800 inside the image when unset. # MIGRATE_STEP_TIMEOUT= # Safety valve for custom/private images without an # org.opencontainers.image.revision label. Leave this unset in production; the # deploy script uses the label to prevent mixing fresh SuperSync deploy inputs # with a stale supersync image. # SUPERSYNC_SKIP_IMAGE_REVISION_CHECK=false