Environment Variables
Every backend setting (env var), what it controls, and the safe production value.
Last updated Thu Jul 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)
TL;DR -This is the master list of the backend's "settings dials" -the environment variables that control everything from the database connection to SMS delivery to in-app-purchase verification -with the correct production value for each.
Who this is for -DevOps & Operations: your reference sheet, keep it open when editing a server config. Engineering: the source of truth when adding a new variable. Executive & Product: skim the section headings to see what moving parts exist.
An environment variable is a named setting the server reads when it starts —
like DATABASE_URL or PORT. Rather than hard-coding secrets and hostnames into
the app, we keep them in a single settings file so the same code can run in
development, staging, and production just by swapping the file. That file is
.env.production and it lives only on the server, never in the code repo,
because it holds real secrets.
Two example templates in the repo document every variable without any real
secrets: backend/.env.example (development) and .env.production.example
(production, at the repo root). When someone adds a new variable, they add it to
these templates in the same change -that keeps this page and the templates
honest.
The rest of this page is the full reference, grouped by area. Each table lists the variable, what it does, and the value to use in production.
Core
The essentials: what mode the server runs in, which port it listens on, how it reaches the database, and the secrets used to sign login tokens.
| Variable | Purpose | Production |
|---|---|---|
NODE_ENV | Enables/disables every dev override | production |
PORT | API port | 3000 |
DATABASE_URL | Postgres connection | composed in docker-compose |
PUBLIC_APP_URL / CORS_ORIGIN | Allowed origins | real hosts only |
API_DOMAIN / ADMIN_DOMAIN / LIVEROOM_DOMAIN / DOCS_DOMAIN | Hostnames used by scripts/provision-server.sh for nginx + certbot (not read by the backend) | e.g. api.social-live.app, docs.owletvpn.com |
LETSENCRYPT_EMAIL | certbot registration/expiry-notice address (provision script only) | set |
JWT_ACCESS_SECRET / JWT_REFRESH_SECRET | Token signing | long random strings |
OTP & messaging
These control how users receive login codes (by SMS or email), how social sign-in
is validated, and how push notifications are sent. The OTP_TEST_* variables are
QA conveniences that must be pruned before launch.
| Variable | Purpose | Production |
|---|---|---|
OTP_SECRET | OTP hash pepper -fails closed if unset in prod | long random string |
TERMS_UPDATED_AT | ISO date the Terms of Use last changed; users with older acceptedTermsAt must re-accept in-app (see Authentication) | empty (code default) |
OTP_ALLOW_UNSENT | Succeed without SMS/email provider | false |
OTP_EXPOSE_CODE | (Dev) echo codes in responses | false (ignored in prod anyway) |
OTP_TEST_CODE | (Dev) fixed code for everyone | empty in prod |
OTP_TEST_PHONE_NUMBERS | QA phone allowlist | prune before launch |
OTP_TEST_EMAILS / OTP_TEST_EMAIL_CODE | QA email allowlist + fixed code | prune before launch |
TWILIO_ACCOUNT_SID / AUTH_TOKEN / FROM_NUMBER | SMS delivery | set |
RESEND_API_KEY / RESEND_FROM_EMAIL | Email delivery | set |
APPLE_SIGNIN_CLIENT_IDS | Allowed audiences for Sign in with Apple tokens (bundle id / Service ID) | com.sociallive.app |
GOOGLE_SIGNIN_CLIENT_IDS | Allowed audiences for Google ID tokens (iOS + web/server client ids) | set |
FIREBASE_SERVICE_ACCOUNT_BASE64 | Base64 Firebase service-account JSON for FCM push (Android + iOS via APNs). Empty disables push silently | set |
Media (SFU)
The SFU is the media server that relays one broadcaster's live video and audio out to many viewers. These variables tell it which network address to advertise and which UDP ports it may use for real-time media.
| Variable | Purpose |
|---|---|
SFU_LISTEN_IP | Bind address (0.0.0.0 in container) |
SFU_ANNOUNCED_IP | Public IP written into ICE candidates |
SFU_RTC_MIN_PORT / MAX_PORT | Published UDP range (40000–40499 in production). One port per WebRTC transport (~2 per participant); a 101-port range exhausts under a modest stress run (no more available ports in the backend log). Keep the range >= 500 wide and mirror it in the ufw rule. |
In-app purchases
These verify real money purchases with Apple and Google before any coins are credited. Getting them right is what stops fraudulent or replayed receipts.
| Variable | Purpose |
|---|---|
APPLE_IAP_ISSUER_ID / KEY_ID / PRIVATE_KEY | App Store Connect API key (ES256 .p8) |
APPLE_IAP_BUNDLE_ID | Expected bundle id (com.sociallive.app) |
APPLE_IAP_ENVIRONMENT | production or sandbox |
GOOGLE_IAP_CLIENT_EMAIL / PRIVATE_KEY | Play service account |
GOOGLE_IAP_PACKAGE_NAME | Android applicationId (com.sociallive.app) |
GOOGLE_RTDN_AUDIENCE | Expected OIDC audience = the RTDN push URL |
GOOGLE_RTDN_SERVICE_ACCOUNT_EMAIL | Expected Pub/Sub push identity |
GOOGLE_IAP_VOIDED_SWEEP_HOURS | Refund sweep interval (0 = off; prod 6) |
ALLOW_DEV_COIN_GRANTS | Dev coin grants without a store receipt -must be false in production once real IAP is live |
Misc
Everything else: diagnostics toggles, payout scaffolding, object storage for media, the admin login token, the docs container binding, database bootstrap, and the offsite backup destination.
| Variable | Purpose |
|---|---|
ENABLE_LIVE_DIAGNOSTICS | Persist live room event logs |
LIVE_PERF_LOGS | Verbose live-path performance logging |
STRIPE_SECRET_KEY / STRIPE_CONNECT_CLIENT_ID | Payout rails (scaffolded) |
S3_ENDPOINT / S3_BUCKET / S3_REGION / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY | Object storage for media (Cloudflare R2 in prod, MinIO in dev). Unset = media falls back to local disk |
S3_PUBLIC_BASE_URL | Optional public bucket/CDN base for avatars/banners; empty = served through the backend /api/media proxy |
ADMIN_ACCESS_TOKEN | Admin portal login token |
DOCS_BIND / DOCS_PORT | Loopback bind + host port for the public docs portal container (3013 in prod); nginx fronts it. No token -the docs site is public and read-only |
POSTGRES_DB / USER / PASSWORD | Database bootstrap |
BACKUP_RCLONE_REMOTE | Offsite backup destination (rclone remote path, e.g. b2:social-live-backups); read by scripts/db-backup.sh, not the backend -see Backup & Restore |
Conventions
A few rules that apply across all of the above:
- Multiline keys (
*_PRIVATE_KEY) may be stored single-line with\nescapes -services normalize them. - Env-only changes deploy with
up -d <service>(no--build). docker-compose.prod.ymlhas no URL fallback defaults:PUBLIC_APP_URL,ADMIN_API_BASE_URL, and the twoLIVE_ROOM_SIMULATOR_*URLs are required (${VAR:?}), and every compose command needs--env-file .env.production-see Server Provisioning.- Never commit real values; the two example files are the documentation of record for new variables -update them in the same PR that adds one.
Related pages
- Infrastructure -the containers these variables configure.
- Server Provisioning -the script that reads the domain/certbot variables.
- Backup & Restore -where
BACKUP_RCLONE_REMOTEis used. - Authentication -the login flow the OTP and JWT variables drive.