Environment Variables
Every variable, which service reads it, and its default. Sourced from each service's config module and .env.example, cross-checked against infra/k8s/*-deployment.yaml.
Core
| Variable | Services | Default | Notes |
|---|---|---|---|
NODE_ENV | all | development | production in the cluster |
ENV | api | development | Separate from NODE_ENV; only the API reads it |
PORT | api, esu, anansi, igdb-heartbeat, web | varies | api 3000→set to 4000, esu 4010, anansi 4005, igdb 4006, web 3000 |
DATABASE_URL | api, esu, ogun, anansi, igdb-heartbeat | — | Required. postgresql://user:pass@host:5432/db |
Redis
| Variable | Services | Default | Notes |
|---|---|---|---|
REDIS_URL | api, esu, ogun | — | Takes precedence over host/port where both are read |
REDIS_HOST | api, esu, ogun, anansi, queue | localhost | |
REDIS_PORT | api, esu, ogun, anansi, queue | 6379 | |
REDIS_PASSWORD | api, esu, ogun, anansi, queue | — | playpalz locally — the Compose Redis requires it |
Authentication
| Variable | Services | Default | Notes |
|---|---|---|---|
JWT_SECRET | api, esu | "supersecretkey" / "" | Must be identical and must be set in production |
RESET_SECRET | api | "supersecretkey" | Password reset tokens |
JWT_EXPIRATION_MINUTES | api | 60 | Read into config but unused — generateToken hard-codes 7 days |
JWT_ALG | esu | HS256 |
The JWT defaults are publicly known
apps/api/src/configs/index.ts falls back to "supersecretkey". Anyone who knows that string can mint a valid token for any user. Production must set it from the api-secrets sealed secret.
Interservice
| Variable | Services | Default | Notes |
|---|---|---|---|
REALTIME_SERVICE_URL | api, ogun | — | Means two different things — see below |
REALTIME_ADMIN_TOKEN | api, esu, ogun | "" | Shared secret for /admin |
API_BASE_URL | esu | http://localhost:3000/api/v1 | ⚠️ Set to :4000 locally |
API_SERVICE_TOKEN | esu | "" | Sent as X-Service-Token |
WEB_APP_URL | api | http://localhost:3000 | CORS origin |
REALTIME_SERVICE_URL is inconsistent
The API's helper appends /admin; ogun's does not:
fetch(`${REALTIME_SERVICE_URL}/admin${path}`) // apps/api/src/lib/interservice.ts
fetch(`${REALTIME_SERVICE_URL}${path}`) // apps/ogun/src/utils/interservice.tsBoth deployments set http://playpalz-esu:4010, which is correct for the API and wrong for ogun. Both .env.example files ship http://localhost:4010/admin, which is correct for ogun and wrong for the API. See Service Topology.
Object storage
The variable names differ between services — an artifact of them being written at different times.
| Variable | Services | Notes |
|---|---|---|
SPACES_REGION | ogun, igdb-heartbeat | e.g. nyc3 |
SPACES_ENDPOINT | ogun | Read by ogun |
SPACES_URL | ogun, igdb-heartbeat | Read as the endpoint by igdb-heartbeat |
SPACES_ACCESS_KEY | ogun, igdb-heartbeat | |
SPACES_SECRET_KEY | ogun, igdb-heartbeat | |
SPACES_BUCKET | api, ogun, igdb-heartbeat | |
PUBLIC_CDN_URL | igdb-heartbeat | CDN base for cover images |
AWS_ACCESS_KEY_ID | api | Alternative naming in .env.example |
AWS_SECRET_ACCESS_KEY | api | |
AWS_REGION | api | |
AWS_S3_BUCKET | api |
The spaces-credentials Kubernetes secret (created by Terraform) supplies AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_ENDPOINT, S3_URL, S3_BUCKET, and CDN_ENDPOINT. Deployments map those into whichever names their service expects. Set both families locally to avoid surprises.
Mux
| Variable | Services | Notes |
|---|---|---|
MUX_ACCESS_TOKEN | api, ogun | |
MUX_SECRET_KEY | api, ogun | |
MUX_WEBHOOK_SECRET | api | Configured but never verified — see Webhooks |
Two secret shapes for Mux
ogun-deployment.yaml still reads a mux-credentials secret with TOKEN_ID / TOKEN_SECRET, while the sealed mux-secrets uses MUX_ACCESS_TOKEN / MUX_SECRET_KEY. Reconcile onto one.
LiveKit
| Variable | Services | Notes |
|---|---|---|
LIVEKIT_HOST | api | HTTP API base |
LIVEKIT_WS_URL | api | Client WebSocket URL |
LIVEKIT_API_KEY | api | devkey locally |
LIVEKIT_API_SECRET | api | secret locally |
Payments
| Variable | Services | Default | Notes |
|---|---|---|---|
REVENUECAT_WEBHOOK_SECRET | api | — | If unset, the webhook accepts anything |
STRIPE_SECRET_KEY | api, anansi | "" | Platform account |
STRIPE_PUBLISHABLE_KEY | api | — | |
PLATFORM_FEE_PCT | anansi | 20 | Integer percent |
MIN_PAYOUT_CENTS | anansi | 100 | $1.00 |
COST_PER_COIN | anansi | 0.02 | USD per Play Coin |
PAYOUT_LOCK_TTL_MS | anansi | 3600000 | Redis lock TTL |
ADMIN_SECRET | anansi | "" | Empty default — set it |
IGDB
| Variable | Services | Notes |
|---|---|---|
IGDB_CLIENT_ID | igdb-heartbeat | Twitch application |
IGDB_CLIENT_SECRET | igdb-heartbeat |
esu
| Variable | Default | Notes |
|---|---|---|
CORS_ORIGIN | * | Comma-separated |
SOCKET_EVENT_LOGGING | true | Logs every socket event |
ADMIN_UI_ENABLED | false | Socket.IO Admin UI |
ADMIN_UI_USERNAME | "" | Required when enabled |
ADMIN_UI_PASSWORD_HASH | "" | bcrypt hash; generate with pnpm --filter @playpals/esu admin-ui:creds |
web (build-time)
| Variable | Production | Notes |
|---|---|---|
NEXT_PUBLIC_API_URL | https://api.playpalz.gg/api/v1 | Must include /api/v1 |
NEXT_PUBLIC_API_TIMEOUT | 10000 | ms |
HOSTNAME | 0.0.0.0 | Required in a container |
NEXT_TELEMETRY_DISABLED | 1 |
NEXT_PUBLIC_* values are inlined at build time. Changing one requires a rebuild — setting it in the deployment manifest alone does nothing for client code.
mobile (build-time)
| Variable | Notes |
|---|---|
EXPO_PUBLIC_ENV | development | staging | production — drives app name and scheme |
EXPO_PUBLIC_API_URL | Must include /api/v1 |
EXPO_PUBLIC_REALTIME_URL | esu base URL, no path |
BUGSNAG_API_KEY | Set per EAS profile |
Baked in at build time by the EAS profile. See Builds & Releases.
Minimum local set
To get the stack running with everything else stubbed:
# packages/database/.env
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# apps/api/.env
PORT=4000
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
REDIS_URL="redis://:playpalz@localhost:6379"
JWT_SECRET="local-dev-secret"
RESET_SECRET="local-dev-reset"
REALTIME_SERVICE_URL="http://localhost:4010"
REALTIME_ADMIN_TOKEN="changeme"
WEB_APP_URL="http://localhost:3000"
# apps/esu/.env
PORT=4010
JWT_SECRET="local-dev-secret" # must match the API
API_BASE_URL="http://localhost:4000/api/v1"
REALTIME_ADMIN_TOKEN="changeme"
REDIS_URL="redis://:playpalz@localhost:6379"
# apps/mobile/.env
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_API_URL=http://localhost:4000/api/v1
EXPO_PUBLIC_REALTIME_URL=http://localhost:4010Note the two deviations from the shipped .env.example files: REALTIME_SERVICE_URL without /admin, and API_BASE_URL on port 4000.
Adding a variable
Four places, every time:
- The service's config module (
src/configs/index.tsorsrc/config/index.ts) - That service's
.env.example infra/k8s/<service>-deployment.yaml— plus a sealed secret if it is sensitive- This page
