Configuration¶
Semaphore Chat is configured through environment variables. The backend reads from backend/.env and the frontend from frontend/.env.
Backend environment variables¶
Copy backend/env.sample to backend/.env to get started.
Core¶
| Variable | Description | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | postgresql://semaphore:semaphore@postgres:5432/semaphore |
JWT_SECRET | Secret key for signing access tokens | (must change) |
JWT_REFRESH_SECRET | Secret key for signing refresh tokens | (must change) |
REDIS_HOST | Redis hostname | redis |
Change the JWT secrets
The default secrets in env.sample are placeholders. Always generate strong random values for production:
File storage¶
File uploads (message attachments, avatars, banners, custom emoji, soundboard sounds) can be stored on the local filesystem or in an S3-compatible object store. Storage is resolved per file record — switching STORAGE_TYPE only affects where new uploads land; existing files keep working from wherever they were originally stored, so mixed-storage instances (e.g. after a mid-life migration to S3) work with zero migration.
| Variable | Description | Default |
|---|---|---|
STORAGE_TYPE | LOCAL or S3 | LOCAL |
FILE_UPLOAD_DEST | Local staging directory. For STORAGE_TYPE=LOCAL this is also the permanent storage location; for STORAGE_TYPE=S3 it's scratch space only (files are uploaded to the bucket, then removed) | ./uploads |
The following are only required when STORAGE_TYPE=S3 (validated at startup):
| Variable | Description | Example |
|---|---|---|
S3_BUCKET | Target bucket name | semaphore-chat-uploads |
S3_REGION | AWS region (or an arbitrary value for MinIO) | us-east-1 |
S3_ACCESS_KEY_ID | Access key | |
S3_SECRET_ACCESS_KEY | Secret key | |
S3_ENDPOINT | (Optional) Custom endpoint — set for S3-compatible services like MinIO | http://minio:9000 |
S3_FORCE_PATH_STYLE | (Optional) Set true for MinIO and most self-hosted S3-compatible services (path-style addressing) | true |
The backend always streams file bytes, never redirects
Uploads and downloads stream through the backend rather than presigning direct-to-S3 URLs, so the existing file access-control guards stay the single source of truth. Presigned URLs are a possible future optimization for read-heavy deployments, not implemented yet.
Local development with MinIO¶
The dev Docker Compose stack includes a MinIO service (S3-compatible, runs locally) behind an opt-in s3 profile so it doesn't start by default:
Then set in backend/.env:
STORAGE_TYPE=S3
S3_BUCKET=semaphore-dev
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_ENDPOINT=http://minio:9000
S3_FORCE_PATH_STYLE=true
The minio-init sidecar creates the semaphore-dev bucket automatically on first start. MinIO's web console (if you enable the console port in docker-compose.yml) is available at http://localhost:9001 with the same credentials.
LiveKit (voice/video)¶
These are optional — voice and video features are disabled if not configured.
| Variable | Description | Example |
|---|---|---|
LIVEKIT_URL | LiveKit server URL returned to the browser for WebRTC connections | wss://your-livekit-server.com |
LIVEKIT_INTERNAL_URL | (Optional) Internal URL for server-to-server LiveKit API calls. Falls back to LIVEKIT_URL if not set. Useful when LiveKit is on a Docker network (e.g., http://livekit:7880) while browsers need a different URL. | http://livekit:7880 |
LIVEKIT_API_KEY | LiveKit API key | your-api-key |
LIVEKIT_API_SECRET | LiveKit API secret (also used to verify webhook signatures — must be at least 32 characters) | your-api-secret |
Replay buffer¶
Configuration for the replay buffer / screen recording feature. Requires LiveKit egress to be set up.
| Variable | Description | Default |
|---|---|---|
REPLAY_SEGMENTS_PATH | Backend storage path for replay metadata | /app/storage/replay-segments |
REPLAY_EGRESS_OUTPUT_PATH | LiveKit egress output path (must be accessible by egress pods) | /out |
REPLAY_SEGMENT_CLEANUP_AGE_MINUTES | How long to keep replay segments before cleanup | 20 |
REPLAY_ORPHAN_SWEEP_ENABLED | Hourly reconciliation sweep that deletes segment directories no session references anymore (crashed recordings, missed webhooks, failed cleanups). Set false to disable | true |
REPLAY_ORPHAN_SWEEP_GRACE_HOURS | Minimum directory age before the orphan sweep may delete it | 24 |
Reverse proxy¶
If Semaphore Chat runs behind a reverse proxy (Nginx, Traefik, Caddy, a cloud load balancer, etc.), set TRUST_PROXY so that rate-limiting and session IPs use the real client address instead of the proxy's.
| Variable | Description | Default |
|---|---|---|
TRUST_PROXY | Number of trusted proxy hops, a subnet name, or a specific IP | 1 |
Common values:
| Value | When to use |
|---|---|
1 | Single reverse proxy (Nginx, Traefik, k8s ingress) |
2 | CDN → reverse proxy → Semaphore Chat |
loopback | Proxy runs on the same host (localhost) |
10.0.0.0/8 | Trust a specific internal subnet |
Never use true in production
true trusts all X-Forwarded-For headers, which lets any client spoof their IP and bypass rate-limiting. Always use a hop count or subnet.
See the Express proxy documentation for all supported values.
Dynamic IP watcher¶
Configuration for the optional IP watcher sidecar. See Dynamic IP support for setup instructions.
| Variable | Description | Default |
|---|---|---|
IP_WATCHER_CHECK_INTERVAL | Seconds between external IP checks | 300 |
Push notifications (VAPID)¶
Web Push notifications require VAPID keys. Each instance needs its own unique key pair.
| Variable | Description | Example |
|---|---|---|
VAPID_PUBLIC_KEY | VAPID public key | (generate with command below) |
VAPID_PRIVATE_KEY | VAPID private key | (generate with command below) |
VAPID_SUBJECT | Contact email for VAPID | mailto:admin@your-instance.com |
Generate VAPID keys:
Password reset email (SMTP)¶
Self-service password reset is optional. The feature is auto-disabled — no error at startup, the reset endpoints and UI simply stay off — unless SMTP_HOST, SMTP_FROM, and PUBLIC_APP_URL are all set.
| Variable | Description | Default |
|---|---|---|
SMTP_HOST | Hostname of the SMTP server | (unset — feature disabled) |
SMTP_PORT | SMTP port | 587 |
SMTP_SECURE | true for implicit TLS (typically port 465). false uses STARTTLS. | false |
SMTP_USER | (Optional) SMTP auth username | |
SMTP_PASS | (Optional) SMTP auth password | |
SMTP_FROM | From address for outbound mail, e.g. "Semaphore Chat <noreply@your-instance.com>" | (unset — feature disabled) |
PUBLIC_APP_URL | Public base URL of the instance, used to build password-reset links in emails, e.g. https://chat.your-instance.com | (unset — feature disabled) |
SMTP_USER and SMTP_PASS
These must be set together or not at all — startup validation fails if only one is provided.
SMTP_HOST requires SMTP_FROM
Startup validation fails if SMTP_HOST is set without SMTP_FROM.
PUBLIC_APP_URL has a second use
It's also used to build absolute incoming-webhook execution URLs. When unset, webhook URLs are returned as relative API paths instead.
GIF search (Giphy)¶
| Variable | Description | Default |
|---|---|---|
GIPHY_API_KEY | Giphy API key — get a free one at developers.giphy.com/dashboard | (unset — feature disabled) |
Disabled when absent
Without GIPHY_API_KEY, the GIF picker is hidden in the UI and the /gifs endpoints return 503.
TENOR_API_KEY from v0.4.0 is deprecated and ignored — replace it with GIPHY_API_KEY.
Background jobs (BullMQ)¶
Message-notification fan-out and link previews run on a BullMQ background queue. It reuses the app's existing REDIS_HOST / REDIS_PORT / REDIS_PASSWORD / REDIS_DB variables but opens its own Redis connection (BullMQ requires maxRetriesPerRequest: null). Processors run in-process in the backend container — no separate worker deployment is needed.
| Variable | Description | Default |
|---|---|---|
JOB_WORKER_CONCURRENCY | Max jobs processed in parallel per queue per process | 4 |
CHANNEL_MESSAGE_MEMBER_THRESHOLD | Max community size for CHANNEL_MESSAGE notification fan-out. Public channels in communities above this member count skip fan-out (logged as a warning) to protect the database on self-hosted instances | 5000 |
Thumbnail backfill¶
On startup, Semaphore Chat retries thumbnail generation for video files that don't have one yet (e.g. uploaded before thumbnail support existed, or whose generation failed). This is deferred and batched so it can't spike memory at boot: at most one ffmpeg frame-extraction child process runs at a time (capped at 30s), and the database working set per batch is just THUMBNAIL_BACKFILL_BATCH_SIZE rows of { id, storagePath }.
| Variable | Description | Default |
|---|---|---|
THUMBNAIL_BACKFILL_ENABLED | Set to false to disable the backfill entirely | true |
THUMBNAIL_BACKFILL_BATCH_SIZE | Rows fetched per batch (id-cursor pagination) | 25 |
THUMBNAIL_BACKFILL_STARTUP_DELAY_MS | Delay before starting, so it runs after peak startup memory has settled | 60000 |
THUMBNAIL_BACKFILL_THROTTLE_MS | Delay between files, to spread out ffmpeg/DB load | 1000 |
Frontend environment variables¶
Copy frontend/.env.sample to frontend/.env. The defaults work for local Docker development.
| Variable | Description | Default |
|---|---|---|
VITE_API_URL | Backend API URL (Vite proxies this in dev; nginx proxies in production) | /api |
VITE_WS_URL | WebSocket URL for Socket.IO | http://localhost:3000 |
Production considerations¶
- JWT secrets — Use long, random strings. Never reuse across environments.
- PostgreSQL — Use authentication and restrict network access.
- Redis — Enable authentication and restrict network access.
- HTTPS — Always use TLS in production. Configure via your reverse proxy or Kubernetes ingress.
- VAPID keys — Generate once per instance and keep stable. Changing them invalidates existing push subscriptions.