Configuration reference
- 1 file
.env, next todocker-compose.yml - 1 command
docker compose up -dapplies a change - 600 the mode to keep
.envat
Everything lives in one file, .env, next to docker-compose.yml. firstboot.sh writes it
for you; this page is for changing it afterwards.
- Apply a change with
docker compose up -d. It recreates only the containers whose settings changed. - Anything marked Never share is a secret. Keep
.envat mode600, keep it out of tickets and chat, and do not paste it into a support request. - A variable with no default listed is off or empty until you set it.
Core and host
Section titled “Core and host”| Variable | What it does | Default | Change it when |
|---|---|---|---|
PUBLIC_HOST |
The hostname this install answers on. The gateway gets a Let’s Encrypt certificate for it on first boot. Needs a DNS record and ports 80 and 443 reachable. More than one name is allowed as a comma-separated list, but then the value must be quoted. | unset (serves localhost on an internal certificate) |
You go live. |
PUBLIC_BASE_URL |
Reachable base URL of the API (avatar URLs, auth callbacks). With PUBLIC_HOST set, use https:// plus that host. |
http://127.0.0.1:3001 |
You go live. |
APP_URL |
Where users actually reach the app. Drives auth and invite redirects and CORS. The server refuses to boot in production if this is localhost. |
http://127.0.0.1:3001 |
You go live. |
CORS_ORIGIN |
Origins allowed to call the API, comma-separated. | APP_URL |
The desktop app signs in but cannot join a call. Its renderer has its own origin, and a missing entry fails silently on the server side. |
NODE_IP |
Address clients use to reach this host for media. Must be routable from the client. | 127.0.0.1 |
Always, for anything but a single-machine test. Set it to the host’s LAN or public IP, or calls negotiate and carry no audio. |
LIVEKIT_PUBLIC_URL |
How clients reach the media server. | ws:// plus NODE_IP and LK_PORT_HTTP |
You go live: wss://your-host. |
SERVER_PORT |
Host port for the API. | 3001 |
A port clash. |
GATEWAY_PORT |
Host port for the gateway. | 8000 |
A port clash. |
LK_PORT_HTTP, LK_PORT_TCP, LK_PORT_UDP |
Host ports for the media server. | 7880, 7881, 7882 |
A port clash. |
LK_PORT_TURN |
Embedded TURN port (UDP). The same port is used inside and out. | 3478 |
A port clash. |
LK_TURN_RELAY_START, LK_TURN_RELAY_END |
Published TURN relay range (UDP). Must match turn.relay_range_start and turn.relay_range_end in livekit.yaml; widen both together or neither. |
30000 to 30063 |
You need more relay capacity. |
HTTP_PORT, HTTPS_PORT |
The public edge. Change only to sit behind something else that owns 80 and 443. TLS then becomes that thing’s job, because the Let’s Encrypt challenge is served on port 80. | 80, 443 |
Almost never. |
AUTH_PUBLIC_URL |
Browser-facing address for the sign-in redirect hop. The public gateway, never the internal one. | PUBLIC_BASE_URL |
Almost never. |
TEAMHUD_VERSION |
Image tag for the server, game server, recorder and draft service. | stable |
You pin a version, or you are on edge before the first stable release. |
TEAMHUD_IMAGE, TEAMHUD_GAME_IMAGE, TEAMHUD_RECORDER_IMAGE, TEAMHUD_DRAFT_IMAGE |
Image repositories. | ghcr.io/arimus/teamhud-server, -game-server, -recorder, -draft-asr |
You mirror the images. |
LIVEKIT_IMAGE, EGRESS_IMAGE |
Media server and egress images. | livekit/livekit-server:v1.13.5, livekit/egress:v1.10.0 |
You run a patched build. |
GIT_COMMIT |
Build identifier reported by the server. deploy.sh sets it from the checked-out commit. |
unknown |
Never by hand. |
MAILER_AUTOCONFIRM |
Confirm users on sign-up. Use true for a closed deployment with no outbound mail. |
true |
You want email confirmation. |
TRUST_PROXY_HOST |
The peer the server trusts for a real X-Forwarded-For: Caddy, by compose service name. |
gateway |
You should not need to. |
TRUST_PROXY |
Explicit IP or CIDR allowlist of trusted proxies. Wins over TRUST_PROXY_HOST. |
unset | Something else, such as a load balancer, sits in front of Caddy. Caddy then also needs trusted_proxies. |
TRUST_PROXY_HOPS |
Retired. A hop count is no longer honoured (it let anyone forge X-Forwarded-For). Setting it only logs a warning. |
none | Remove it if you set it. |
RATE_LIMIT_STORE |
Where rate limits and sign-in lockouts are counted: memory or postgres. |
memory |
You run more than one server. Set postgres on every one, or each counts only its own traffic. The 600-requests-a-minute flood guard always counts in memory. |
CALL_DIAG_TOKEN |
Retired. One shared token could not say who read a call’s diagnostics. A System admin now mints a token per person or tool (POST /api/admin/call-diagnostics/tokens with a label and their password; shown once, expires, revocable), and every read is recorded. Setting it only logs a warning. |
none | Remove it if you set it. |
CALL_DIAG_READS_RETENTION_MONTHS |
Months to keep the record of who read a call’s diagnostics, when, and from where (never the diagnostics themselves). Older rows are deleted automatically each day by the server. Whole number from 3 to 120; anything lower stops the server from starting. | 12 |
Set the same value on every instance. |
Secrets
Section titled “Secrets”Generated by node gen-keys.mjs >> .env. All are Never share.
| Variable | What it does | Default | Change it when |
|---|---|---|---|
POSTGRES_PASSWORD |
Database password. Also the way into the database from the box’s shell. | generated | Rarely. |
JWT_SECRET |
Signs sign-in tokens. | generated | Regenerate with ANON_KEY and SERVICE_ROLE_KEY. |
ANON_KEY, SERVICE_ROLE_KEY |
Tokens derived from JWT_SECRET. The service-role key is full database access. |
generated | Regenerate together with JWT_SECRET. |
LIVEKIT_API_KEY, LIVEKIT_API_SECRET |
Credentials between the server and the media server. | generated | Rotate together. |
ENCRYPTION_KEY |
Encrypts per-org credentials at rest (an org’s own transcription key, its own media provider). Exactly 64 hex characters. Without it, those routes fail with “ENCRYPTION_KEY is not configured” while the rest of the stack looks healthy. | generated | Never casually. Changing it makes stored per-org credentials unreadable. |
AUTH_DB_PASSWORD, STORAGE_DB_PASSWORD, TEAMHUD_APP_DB_PASSWORD |
Passwords of the database roles used by sign-in, file storage and the server. | generated | Rarely. |
REDIS_PASSWORD |
Password of the Redis that egress uses. | generated | Rarely. |
DRAFTS_API_KEY |
The key the server, recorder and draft service share for live caption drafts. Never the same as RECORDER_TOKEN; at least 16 characters. |
generated | Rotate together on all three. |
RECORDER_TOKEN |
Shared between the server, recorder and draft service. Without it, recordings capture nothing and the UI says so. | generated | Rotate together on all three. |
GAME_SERVER_SECRET |
Signs mini-game join tokens. Only with the game server. | generated by firstboot.sh if you opt in |
You rotate it. |
ENT_SECRET |
Signs entitlements and licences. Required for the Slack doorbell: without it every subscribe is rejected with no_ent_secret and clients fall back to polling. Any long random string. |
unset | You use Slack or Teams live updates. |
Access and sign-up
Section titled “Access and sign-up”| Variable | What it does | Default | Change it when |
|---|---|---|---|
SIGNUP_MODE |
Who may create an account: open, sso_only or closed (invite-only: only someone invited by email can create an account). See Users, roles and sign-up. Apply with docker compose up -d, not deploy.sh, which recreates only the server and leaves GoTrue’s own sign-up unchanged. |
open |
You want to restrict sign-up. |
DISABLE_SIGNUP |
Legacy. true behaves as SIGNUP_MODE=closed (invite-only). firstboot.sh writes true unless you choose open sign-up. If both are set, SIGNUP_MODE wins. |
false |
Prefer SIGNUP_MODE. |
GOTRUE_DISABLE_SIGNUP |
GoTrue’s own sign-up flag. Follows DISABLE_SIGNUP when unset. |
follows DISABLE_SIGNUP |
On an SSO-only box set it false, because GoTrue has to be able to create a user on the sign-in callback. This is not an open door: the public site forwards only GoTrue’s browser hops. Set true for a fully closed box. |
BOOTSTRAP_ADMIN_EMAIL |
Email of the first System admin. Acts only while the platform has no admin; the server creates the account and prints its password once. | unset | You want the first System admin created at boot instead of through the setup link. |
SETUP_TOKEN |
One-time token for /setup. Until the first team exists the server answers nothing else without it: no sign-up, sign-in, call or invite (403 setup_required). Inert after. Never share. |
generated by firstboot.sh |
You install by hand and want the setup link. |
TEAMHUD_REQUIRE_SETUP |
A self-hosted server with no team yet and no SETUP_TOKEN stays shut (it logs how to unlock it at boot). false turns off only that case; a set SETUP_TOKEN still locks. Leave it on. |
true |
Only a disposable test stack that starts empty with no token (set false). |
ADMIN_IP_ALLOWLIST |
IPs and IPv4 CIDRs, comma-separated, allowed to open /admin and /api/admin/*. Fails closed: unset means the admin pages answer 404 to everyone. |
unset | Always set it. |
MIN_CLIENT_VERSION |
Oldest desktop version accepted without an “Update TeamHUD” nudge. Soft: an older client is flagged at sign-in, never refused. | unset (no floor) | You want everyone on a minimum version. |
AUTH_THROTTLE_EXEMPT_IPS |
Exact client addresses (comma-separated, no ranges) that the sign-in, sign-up and password-reset failure throttles never count or refuse. Meant for a test runner. | unset | Never for real users. Do not point it at a range real users share. |
RATE_LIMIT_GLOBAL_MAX |
Global requests per client per minute. | 600 |
Test stacks only. |
RESEND_API_KEY, RESEND_FROM |
Invite emails through Resend. Without a key the invitation and its shareable link are still created; only the email is skipped. Supabase still sends auth mail. RESEND_API_KEY is Never share. |
unset | You want invite emails. |
Single sign-on
Section titled “Single sign-on”See Slack and Microsoft for the full app setup.
| Variable | What it does | Default | Change it when |
|---|---|---|---|
SOCIAL_PROVIDERS |
Social sign-in offered to clients, for example slack_oidc. A provider also has to be switched on in GoTrue, or it shows no button. |
empty (none) | You turn on Slack or Microsoft sign-in. |
CHANNEL_PROVIDERS |
Lets orgs use Slack’s own channels and DMs (slack). Needs slack_oidc in SOCIAL_PROVIDERS. teams is accepted but ignored until Teams channels are built. |
empty (TeamHUD channels only) | Same. |
GOTRUE_EXTERNAL_SLACK_OIDC_ENABLED |
Switches Slack sign-in on in GoTrue. | false |
You use Slack sign-in. |
GOTRUE_EXTERNAL_SLACK_OIDC_CLIENT_ID |
Slack app client ID. | unset | Same. |
GOTRUE_EXTERNAL_SLACK_OIDC_SECRET |
Slack app secret. Never share. | unset | Same. |
GOTRUE_EXTERNAL_SLACK_OIDC_REDIRECT_URI |
Sign-in callback. | PUBLIC_BASE_URL plus /auth/v1/callback |
Set only to override. |
GOTRUE_EXTERNAL_AZURE_ENABLED |
Switches Microsoft sign-in on in GoTrue. | false |
You use Microsoft sign-in. |
GOTRUE_EXTERNAL_AZURE_CLIENT_ID |
Entra app registration client ID. | unset | Same. |
GOTRUE_EXTERNAL_AZURE_SECRET |
Entra app secret. Never share. | unset | Same. |
AZURE_TENANT |
Pins sign-in to your Entra tenant (id or domain). Unset means common, which lets any work, school or personal Microsoft account sign in. |
common |
Recommended for a private server. |
GOTRUE_EXTERNAL_AZURE_URL |
Full authority URL. Overrides AZURE_TENANT. No /v2.0 suffix. |
https://login.microsoftonline.com/ plus the tenant |
Advanced. |
GOTRUE_EXTERNAL_AZURE_REDIRECT_URI |
Sign-in callback. | PUBLIC_BASE_URL plus /auth/v1/callback |
Set only to override. |
GOTRUE_URI_ALLOW_LIST |
Where GoTrue may send someone after a provider redirect. If a target is missing, GoTrue discards redirect_to and bounces to the site root, with no error. |
this deployment plus the desktop app’s loopback | Sign-in never completes after the provider step. |
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET |
Slack app for channels and the doorbell. The secret and signing secret are Never share. | unset | You use Slack channels. |
SLACK_EVENT_SOURCE |
events needs Slack to reach this box. socket needs no inbound URL but rings channel doorbells only, not DMs. |
off |
You use the Slack doorbell. Prefer events. |
SLACK_APP_TOKEN |
Socket Mode app token (xapp-). Never share. |
unset | Only with socket. |
SLACK_APP_AUDIENCE_TOKEN |
App-level token with authorizations:read. Without it a group DM rings only one participant. Never share. |
unset | You use the doorbell. |
SLACK_REDIRECT_URI |
Slack OAuth callback override. | PUBLIC_BASE_URL plus /slack/oauth/callback |
Slack reaches this box on a different hostname, such as a tunnel. |
MSTEAMS_TENANT_ID, MSTEAMS_CLIENT_ID, MSTEAMS_CLIENT_SECRET |
Separate Entra registration for live Teams updates. Needs ENT_SECRET and admin consent. The secret is Never share. |
unset | You want live Teams updates. See Microsoft. |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GMAIL_PUSH_TOPIC, GMAIL_PUSH_SA_EMAIL, GOOGLE_REDIRECT_URI |
Google and Gmail push. The secret is Never share. | unset | You use Gmail. See Google. |
Storage
Section titled “Storage”The default is local disk under a Docker volume, which is right when the reason you self-host is that data should not leave your building.
| Variable | What it does | Default | Change it when |
|---|---|---|---|
STORAGE_BACKEND |
s3 moves attachments to S3-compatible object storage. |
file |
You prefer object storage. |
STORAGE_WORKSPACE |
Root of every object key. Write-once: changing it later orphans everything already uploaded, silently. | stub |
Set it once when you switch to s3. |
STORAGE_PREFIX |
Optional level above the workspace, for one bucket holding several deployments. | unset | Rarely. |
STORAGE_S3_REGION, STORAGE_S3_BUCKET |
Bucket location. | stub |
You switch to s3. |
STORAGE_S3_ENDPOINT |
The regional endpoint with no bucket in the hostname. | unset | You switch to s3. |
STORAGE_S3_ACCESS_KEY, STORAGE_S3_SECRET_KEY |
Bucket credentials. Never share. | unset | You switch to s3. |
STORAGE_S3_PROTOCOL, STORAGE_S3_FORCE_PATH_STYLE |
Connection style. | https, true |
Rarely. |
STORAGE_FILE_SIZE_LIMIT |
Largest object the storage API accepts, in bytes. storage-api’s own default is 50 MB, which silently breaks the update feed. | 2147483648 (2 GiB) |
Rarely. |
Updates, licence and legal
Section titled “Updates, licence and legal”| Variable | What it does | Default | Change it when |
|---|---|---|---|
TEAMHUD_LICENSE_KEY, TEAMHUD_LICENSE_PUBLIC_KEY |
The licence you were issued and the key that proves we issued it. Both are needed. Verification fails closed and logs nothing, so a missing key runs the trial or free ladder and looks healthy. Check GET /usage, not the logs. The licence key is Never share. |
unset | You hold a licence. |
TEAMHUD_REQUIRE_ACTIVATION |
A self-hosted server that was never activated (no key, pasted or from TEAMHUD_LICENSE_KEY) serves only the activation screen: sign in to TeamHUD Cloud and approve the code against one of your subscriptions, or paste a key. A subscription covers one server plus a backup; a third server on the same key is flagged to you, not blocked. Applies only when TEAMHUD_SELF_HOSTED=true, which the shipped compose sets for you, so a stock self-host is gated with no edits. A server that was activated and later lapses keeps running at Free limits. Leave it on. |
true |
Only a test or dev stack with no key (set false). |
TEAMHUD_LICENSE_SERVER |
Where the daily renewal check goes. | https://dashboard.teamhud.app |
Only if you run your own issuer. |
TEAMHUD_SELF_HOSTED |
Declares whose hardware this is. The shipped compose file defaults it to true, so on your own box you change nothing. false is the metered cloud free tier and also switches the activation requirement off. TeamHUD’s own hosted and dev boxes must set TEAMHUD_SELF_HOSTED=false in their .env before deploying this version, or they are gated. |
true in the compose file (false when the server runs outside compose) |
Never false on your own box. |
TEAMHUD_AMBIENT_ROOM_CAP |
People allowed in one always-on team room. Takes effect only where TEAMHUD_SELF_HOSTED=true. |
15 |
Your hardware and bandwidth can take more. Above about 30, a meeting fits better. |
SELF_HOST_BUNDLE_PATH |
Storage path of the bundle GET /downloads/server serves. Unset: 404. |
unset | Only a server that hands out the server bundle. |
UPDATE_AUTOPIN_ENABLED |
Pins a rollout back when its cohort’s crash or update-failed rate crosses the threshold. | false |
You distribute installers. |
UPDATE_AUTOPIN_THRESHOLD, UPDATE_AUTOPIN_MIN_SAMPLE, UPDATE_AUTOPIN_WINDOW_MINUTES |
Failure rate, minimum beacons, and window. Raise the sample with ENABLED. |
0.2, 20, 360 |
Tuning. |
TEAMHUD_TERMS_URL, TEAMHUD_PRIVACY_URL, TEAMHUD_ACCEPTABLE_USE_URL |
Your own legal documents. Unset, the server declares documents on its own origin (/legal/terms), so users never consent to TeamHUD’s document into your database. |
unset | You have your own terms. |
TEAMHUD_LEGAL_VERSION |
Bump when you amend your terms. It forces every user to re-accept. | unset | You change your terms. |
TEAMHUD_LEGAL_GRACE_DAYS |
Days a user may keep going before accepting, counted from the first time the server tells that user to accept. After that the account gets 403 terms_required. 0 means accept before using it at all. |
14 |
Tighten or relax. |
TEAMHUD_LEGAL_EFFECTIVE_AT |
Optional ISO date before which no grace period starts. | unset | You announce terms ahead of time. |
Optional components
Section titled “Optional components”| Variable | What it does | Default | Change it when |
|---|---|---|---|
ENABLE_GAMES, COMPOSE_PROFILES |
Team mini-games. firstboot.sh asks (default yes). On yes it adds games to COMPOSE_PROFILES and generates GAME_SERVER_SECRET. |
true after firstboot.sh; the profile is off if you never ran it |
You want mini-games. |
GAME_SERVER_URL |
Where the server proxies /game/*. Compose-internal. |
http://game-server:2567 |
Never. |
TURN_TLS_ENABLED, TURN_DOMAIN, TURN_RELAY_START, TURN_RELAY_END |
TURN over TLS on port 443, for clients behind firewalls that allow only outbound 443. Needs a public NODE_IP, free host ports 80 and 443, and a DNS record for TURN_DOMAIN (unless it equals PUBLIC_HOST). firstboot.sh turns the compose override on through COMPOSE_FILE. |
off; relay 30000 to 30199 |
Clients on 443-only networks. LAN or VPN-only orgs leave it off. |
FCM_PROJECT_ID, FCM_CLIENT_EMAIL, FCM_PRIVATE_KEY |
Push notifications to phones, from a Firebase service-account JSON. Quote the key and escape newlines as \n. The key is Never share. |
unset (push off) | You ship a mobile client. |
Recording and transcription
Section titled “Recording and transcription”These reach the server and recorder through docker-compose.yml. They only matter when an
Org owner or Org admin has switched capture on; see Org policies.
| Variable | What it does | Default | Change it when |
|---|---|---|---|
TRANSCRIPTION_PROVIDER |
Deployment-default provider: deepinfra or openai-compatible. Unset means each org configures its own. |
unset | You want one provider for everyone. |
DEEPINFRA_API_KEY |
Key for deepinfra. Never share. |
unset | Same. |
TRANSCRIPTION_BASE_URL, TRANSCRIPTION_MODEL, TRANSCRIPTION_API_KEY |
For openai-compatible, such as a local Speaches or NeMo-Speech server. The key is Never share. |
unset | Same. |
DRAFTS_URL |
Live word-by-word caption drafts service. An empty value turns drafts off, and captions then arrive per sentence. | http://draft-asr:3003 |
Move the load to another box, or turn drafts off. |
DRAFT_WORKERS, DRAFT_MAX_STREAMS |
Draft service workers and concurrent speaker streams. | 2, 16 |
CPU tuning. |
DRAFT_FINAL_MODEL, DRAFT_FINAL_WORKERS, DRAFT_FINAL_THREADS, DRAFT_FINAL_QUEUE |
Second-pass local model. off means drafts only. |
parakeet-v2, 1, unset, 4 |
CPU tuning. |
DIARIZER_URL |
Optional speaker-splitting service for meeting-room mics. Unset means one speaker per mic. | unset | You run one. |
EGRESS_OUTPUT_DIR |
Enables audio and video files of record. Set /recordings with COMPOSE_PROFILES=egress and LIVEKIT_REDIS_ADDRESS=redis:6379. |
empty (no media files; transcription still works) | You want saved media. |
LIVEKIT_REDIS_ADDRESS |
Needed by egress. | empty (single node) | With egress. |
LIVEKIT_URL, LIVEKIT_WEBHOOK_URL |
Internal media-server and webhook addresses. An empty webhook disables it. | ws://livekit:7880, http://server:3001/livekit/webhook |
Never on the stock stack. |
TEAMHUD_ANALYTICS_URL, SFU_METRICS_TOKEN |
Media-server metrics. Same-host only; a self-hosted box phones nothing home. The token is Never share. | unset | Leave unset. |
Air-gapped servers
Section titled “Air-gapped servers”A server with no internet uses an offline key: download it from your TeamHUD Cloud subscription (paid
subscriptions only), typing the workspace name it is for, and paste it in Settings → License or set
TEAMHUD_LICENSE_KEY before first start. Create your first workspace with that exact name. The key is valid
to the end of your subscription term (up to one year), never checks in, and after it expires the server
keeps running at Free limits following a 30-day grace period. Because offline keys never check in, the
two-servers-per-key notice cannot apply to them.
Test-only
Section titled “Test-only”ALLOW_E2E_DEV_MEETINGS, ALLOW_E2E_LOOPBACK_CORS and the E2E_CLOUDFLARE_* variables exist
for the project’s own test stack. Leave them unset on a real install.