Skip to content

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 ​

VariableServicesDefaultNotes
NODE_ENValldevelopmentproduction in the cluster
ENVapidevelopmentSeparate from NODE_ENV; only the API reads it
PORTapi, esu, anansi, igdb-heartbeat, webvariesapi 3000→set to 4000, esu 4010, anansi 4005, igdb 4006, web 3000
DATABASE_URLapi, esu, ogun, anansi, igdb-heartbeat—Required. postgresql://user:pass@host:5432/db

Redis ​

VariableServicesDefaultNotes
REDIS_URLapi, esu, ogun—Takes precedence over host/port where both are read
REDIS_HOSTapi, esu, ogun, anansi, queuelocalhost
REDIS_PORTapi, esu, ogun, anansi, queue6379
REDIS_PASSWORDapi, esu, ogun, anansi, queue—playpalz locally — the Compose Redis requires it

Authentication ​

VariableServicesDefaultNotes
JWT_SECRETapi, esu"supersecretkey" (dev/test only) / ""Must be identical. Outside dev/test the API won't start unless it's ≥ 32 characters
RESET_SECRETapi"supersecretkey" (dev/test only)HMAC key for stored password-reset codes. Same ≥ 32-character rule
JWT_EXPIRATION_MINUTESapi60Lifetime of refreshable (mobile) access tokens; legacy tokens stay 7 days
REFRESH_TOKEN_TTL_DAYSapi90Sliding lifetime of a refresh session, extended on every refresh
JWT_ALGesuHS256

The JWT defaults are publicly known

apps/api/src/configs/index.ts falls back to "supersecretkey" only when ENV is development or test. Anyone who knows that string can mint a valid token for any user, so any other ENV throws at startup when either secret is missing or shorter than 32 characters. Production sets both from the api-secrets sealed secret.

Email ​

VariableServicesDefaultNotes
RESEND_API_KEYapi—Resend API key for transactional email (password reset). Unset → emails are logged in development and dropped with an error log elsewhere
EMAIL_FROMapiPlayPalz <no-reply@playpalz.gg>Sender. Its domain must be verified in Resend

Interservice ​

VariableServicesDefaultNotes
REALTIME_SERVICE_URLapi, ogun—Means two different things — see below
REALTIME_ADMIN_TOKENapi, esu, ogun""Shared secret for /admin
API_BASE_URLesuhttp://localhost:3000/api/v1⚠️ Set to :4000 locally
API_SERVICE_TOKENesu""Sent as X-Service-Token
WEB_APP_URLapihttp://localhost:3000CORS origin
ANANSI_URLapihttp://playpalz-anansi:4005anansi, for the admin panel's Payouts and Jobs pages. http://localhost:4005 locally
ANANSI_ADMIN_SECRETapi—anansi's ADMIN_SECRET. Unset → the Payouts page's runs and retries answer 503

REALTIME_SERVICE_URL is inconsistent

The API's helper appends /admin; ogun's does not:

ts
fetch(`${REALTIME_SERVICE_URL}/admin${path}`)   // apps/api/src/lib/interservice.ts
fetch(`${REALTIME_SERVICE_URL}${path}`)          // apps/ogun/src/utils/interservice.ts

Both 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.

VariableServicesNotes
SPACES_REGIONogun, igdb-heartbeate.g. nyc3
SPACES_ENDPOINTapi, ogun, igdb-heartbeatRegional S3 API endpoint, https://nyc3.digitaloceanspaces.com
SPACES_URLapi, ogun, igdb-heartbeatBucket origin, https://<bucket>.nyc3.digitaloceanspaces.com
SPACES_ACCESS_KEYogun, igdb-heartbeat
SPACES_SECRET_KEYogun, igdb-heartbeat
SPACES_BUCKETapi, ogun, igdb-heartbeat
PUBLIC_CDN_URLapi, igdb-heartbeatCDN base for public files. Production maps it from CDN_ENDPOINT, a bare host; igdb-heartbeat adds https://
AWS_ACCESS_KEY_IDapiAlternative naming in .env.example
AWS_SECRET_ACCESS_KEYapi
AWS_REGIONapi
AWS_S3_BUCKETapi

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.

Creator KYC storage ​

A separate private bucket for government ID images. See Creator Onboarding & KYC. There is no fallback to SPACES_*: ID uploads return 503 unless every variable here is set.

VariableServicesNotes
KYC_SPACES_BUCKETapiplaypalz-kyc in production. Never the media bucket
KYC_SPACES_REGIONapiFalls back to SPACES_REGION
KYC_SPACES_ENDPOINTapiRegional endpoint, e.g. https://nyc3.digitaloceanspaces.com
KYC_SPACES_ACCESS_KEYapiBucket-scoped key from Terraform
KYC_SPACES_SECRET_KEYapi
KYC_ENCRYPTION_KEYapi32 random bytes, base64 (openssl rand -base64 32). Wraps every per-image key. Losing it makes every stored ID unreadable
KYC_ENCRYPTION_KEY_VERSIONapiDefault 1. Bump on rotation
KYC_ENCRYPTION_KEY_V<n>apiPrevious keys, kept until no rows use version n

Mux ​

VariableServicesNotes
MUX_ACCESS_TOKENapi, ogun
MUX_SECRET_KEYapi, ogun
MUX_SIGNING_KEYapiMux signing key id, for signed playback of VIP videos. Unset → private videos can't play
MUX_PRIVATE_KEYapiThat signing key's private key (base64), as Mux issues it
MUX_WEBHOOK_SECRETapiWebhook signing secret. Unset in production → the webhook answers 503; 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 ​

VariableServicesNotes
LIVEKIT_HOSTapiHTTP API base
LIVEKIT_WS_URLapiClient WebSocket URL
LIVEKIT_API_KEYapidevkey locally
LIVEKIT_API_SECRETapisecret locally

Payments ​

VariableServicesDefaultNotes
REVENUECAT_WEBHOOK_SECRETapi—The webhook's Authorization value. Unset in production → the webhook answers 503; elsewhere it accepts unauthenticated calls
REVENUECAT_V2_SECRET_KEYapi—RevenueCat API v2 secret key with customer_information:purchases:read, for /coins/purchases/sync
REVENUECAT_PROJECT_IDapi—RevenueCat project id, for the same
COIN_PURCHASE_SYNC_ENABLEDapifalse in production, true elsewhereVerify-and-credit on /coins/purchases/sync. Keep off in production until the transaction ids are confirmed to match (see Monetization)
COIN_ALLOW_SANDBOX_PURCHASESapifalse in production, true elsewhereCredit sandbox (free) store purchases. Never enable in production
EARNINGS_HOLD_DAYSapi30How long a PlayPal's earnings (tips, and USD earnings from passes) are held before they can be paid out
STORE_COMMISSION_DEFAULT_PCTapi30Store commission assumed for a pass's earnings until the webhook reports the real one
CHECKOUT_INTENT_TTL_MINUTESapi30How long a checkout waits for its store purchase
PURCHASE_CREDIT_VALID_DAYSapi365How long an unused purchase credit lasts
COIN_PAYOUT_CENTS_PER_1000api450What 1,000 cashed-out coins pay, in cents (the platform's take lives here)
CASHOUT_MIN_COINS / CASHOUT_STEP_COINSapi2000 / 100Smallest cash-out, and the step requests come in
STRIPE_TRANSFER_WEBHOOK_SECRETapi—Signing secret of the Stripe event destination for transfer events. Unset → /webhooks/stripe-transfers answers 503
TIP_MAX_COINSapi10000Largest single tip
TIP_CONFIRM_ABOVEapi500Tips above this need a confirmation step
TIP_DAILY_CAP / TIP_DAILY_CAP_NEW_ACCOUNT / TIP_DAILY_CAP_MINORapi20000 / 2000 / 1000Coins a sender can tip in 24 hours
TIP_NEW_ACCOUNT_DAYSapi7How long an account counts as new
TIP_PER_MINUTEapi30Tips a sender can send per minute
TIP_MESSAGE_BLOCKLISTapi—Comma-separated terms refused in tip messages (whole words; phrases match anywhere)
FRAUD_HOLD_EXTENSION_DAYSapi30Extra hold on earnings from a flagged sender
FRAUD_NEW_ACCOUNT_COINS / FRAUD_CONCENTRATION_MIN_COINS / FRAUD_CONCENTRATION_PERCENTapi1000 / 2000 / 80Abuse-signal thresholds
FRAUD_REFUND_MIN_REFUNDS / FRAUD_REFUND_PERCENTapi2 / 30Flag a buyer with this many refunds in 90 days covering this share of their purchases
STRIPE_SECRET_KEYapi, anansi""Platform account. Use a restricted key (rk_…). The api needs Connect accounts and external accounts write. Creator onboarding returns 503 when unset
STRIPE_PUBLISHABLE_KEYapi—
STRIPE_WEBHOOK_SECRETapi—Event destination signing secret. Unset → the Stripe webhook answers 503 (fails closed)
PLATFORM_FEE_PCTapi, anansi20Platform's cut of store-purchase proceeds, integer percent. The api applies it when recording earnings
MIN_PAYOUT_CENTSanansi100$1.00
PAYOUT_LOCK_TTL_MSanansi3600000Redis lock TTL
ADMIN_SECRETanansi""Empty default — set it

IGDB ​

VariableServicesNotes
IGDB_CLIENT_IDigdb-heartbeatTwitch application
IGDB_CLIENT_SECRETigdb-heartbeat

esu ​

VariableDefaultNotes
CORS_ORIGIN*Comma-separated
SOCKET_EVENT_LOGGINGtrueLogs every socket event
ADMIN_UI_ENABLEDfalseSocket.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) ​

VariableProductionNotes
NEXT_PUBLIC_API_URLhttps://api.playpalz.gg/api/v1Must include /api/v1
NEXT_PUBLIC_API_TIMEOUT10000ms
HOSTNAME0.0.0.0Required in a container
NEXT_TELEMETRY_DISABLED1

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) ​

VariableNotes
EXPO_PUBLIC_ENVdevelopment | staging | production — drives app name and scheme
EXPO_PUBLIC_API_URLMust include /api/v1
EXPO_PUBLIC_REALTIME_URLesu base URL, no path
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEYpk_test_… / pk_live_…. Tokenizes creator PII and bank details on the device. Without it the payout steps are unavailable
EXPO_PUBLIC_POSTHOG_KEYPostHog project key (phc_…) for crash/error reporting. Unset → nothing is reported
EXPO_PUBLIC_POSTHOG_HOSTDefault https://us.i.posthog.com (https://eu.i.posthog.com for EU Cloud)
EXPO_PUBLIC_IOS_REVENUECAT_PUBLIC_KEYRevenueCat public SDK key for the App Store (appl_…), used on iOS. Unset → purchases are unavailable on iOS
EXPO_PUBLIC_ANDROID_REVENUECAT_PUBLIC_KEYRevenueCat public SDK key for Google Play (goog_…), used on Android. Unset → purchases are unavailable on Android

Inlined into the JS bundle. Builds and over-the-air updates read them from the EAS environment of the same name as the profile (eas env:list --environment production), never from eas.json. Locally, apps/mobile/.env. Both RevenueCat keys ship in every bundle and lib/env.ts picks one by Platform.OS. For simulator testing with RevenueCat's Test Store key (test_…), put it in both. See Builds & Releases.

Minimum local set ​

To get the stack running with everything else stubbed:

bash
# 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:4010

Note 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:

  1. The service's config module (src/configs/index.ts or src/config/index.ts)
  2. That service's .env.example
  3. infra/k8s/<service>-deployment.yaml — plus a sealed secret if it is sensitive
  4. This page

Internal documentation — PlayPalz platform