Skip to content

Environments ​

Three environments: your laptop, staging, and production.

EnvironmentWhereStatus
LocalDocker Compose + pnpm devWorking
StagingDOKS staging namespace, *.staging.playpalz.ggLive
ProductionDOKS default namespaceLive

Mobile preview builds still talk to production

The mobile preview profile builds with EXPO_PUBLIC_ENV=staging, but its EAS environment points at the production API and socket hosts. Point it at staging with eas env:set preview --name EXPO_PUBLIC_API_URL --value https://api.staging.playpalz.gg/api/v1 (and EXPO_PUBLIC_REALTIME_URL → https://socket.staging.playpalz.gg), then rebuild the preview app.

Local ​

ServicePortHow
PostgreSQL5432docker compose up -d db
Redis6379docker compose up -d redis (password playpalz)
LiveKit7880–7882docker compose up -d livekit (devkey / secret)
api4000pnpm --filter @playpals/api dev
esu4010pnpm --filter @playpals/esu dev
ogun—pnpm --filter @playpals/ogun dev
anansi4005pnpm --filter @playpals/anansi dev
web3000pnpm --filter @playpals/web dev
docs5173pnpm --filter @playpals/docs dev

pnpm dev starts api, esu, ogun, and mobile together; pnpm dev:all starts everything.

Third-party services degrade gracefully — without Spaces, Mux, RevenueCat, or Stripe credentials the app still runs; those features just do not work. Full setup: Local Setup.

Docker-based local ​

docker-compose.dev.yaml overlays live-reload containers for api, esu, and ogun on top of the base Compose stack:

bash
./start-dev.sh          # foreground, everything
./start-dev.sh -d api   # detached, one service

Slower than running natively, but eliminates environment drift between developers.

Production ​

Namespace default in the DOKS cluster.

ServiceReplicasStrategyIngress
web3RollingUpdatewww.playpalz.gg, playpalz.gg
api3RollingUpdateapi.playpalz.gg
esu2RollingUpdatesocket.playpalz.gg
ogun2 (HPA 2–8)RollingUpdate—
anansi1Recreate—
igdb-heartbeat1Recreate—
redis1 (StatefulSet + 5Gi PVC)——

The two singletons use Recreate rather than RollingUpdate deliberately: both are cron-driven, and briefly running two pods during a rollout risks a duplicate payout run or a duplicate sync.

anansi's Redis lock makes that safe in principle, but Recreate means never having to rely on it.

Staging Live ​

A full copy of the platform for testing changes before production, in the staging namespace of the same cluster. It runs the same Helm charts as production, with infra/helm/values/staging/.

ProductionStaging
Namespacedefaultstaging
Webwww.playpalz.gg, playpalz.ggstaging.playpalz.gg
APIapi.playpalz.ggapi.staging.playpalz.gg
Realtimesocket.playpalz.ggsocket.staging.playpalz.gg
Adminadmin.playpalz.ggadmin.staging.playpalz.gg
Docsdocs.playpalz.ggdocs.staging.playpalz.gg
ReplicasAs above1 each, no autoscaling
DatabaseManaged Postgres 15In-cluster Postgres 15 (playpalz-postgres, 10Gi volume), TLS with a self-signed certificate
Redis5Gi, 512 MB dataset1Gi, 256 MB dataset
Media / KYCplaypalz-media-production, playpalz-kycplaypalz-media-production-staging, playpalz-kyc-staging (own scoped keys)
StripeLive keys (once live)Test mode
RevenueCatProduction webhookSandbox purchases, its own webhook secret
MuxProduction environmentPlaceholder until a Mux development environment is set up: video uploads fail at Mux
LiveKit, IGDB—Shared with production
ENVproductionstaging: secrets still required, sandbox coin purchases accepted

Why staging's Postgres has TLS: the shared database client (packages/database/src/client.ts) always connects with TLS when NODE_ENV=production, which every deployed image is. It ignores sslmode and doesn't verify the certificate, because DigitalOcean's managed database uses a self-signed CA. So staging's Postgres serves TLS too, with a self-signed certificate from the sealed postgres-tls secret. Without it, every query fails with "The server does not support SSL connections", while health checks, which don't touch the database, still pass.

What's different in staging:

  • Email: staging is set up without email, so it shouldn't send any to real people.
  • Socket.IO admin UI: off.
  • Data: the database starts from migrations plus the demo seed (pnpm db:seed).

Deploying: every service and the platform release take --env staging:

bash
TAG=$(git rev-parse --short HEAD)
./scripts/build-api.sh "$TAG"                     # images are shared with production…
./scripts/build-web.sh "$TAG" staging             # …except web, which bakes in the staging API URL
infra/helm/deploy.sh --env staging api "$TAG"
infra/helm/deploy.sh --env staging web "$TAG"     # deploys <tag>-staging

Normally you don't run these: merging to master deploys the commit to staging automatically (Deploy staging workflow), and production is promoted from the same images. The commands above are the manual path, for example for one service on its own.

Re-seeding or resetting staging data. Postgres is only reachable inside the cluster, so seed through a port-forward:

bash
kubectl -n staging port-forward svc/playpalz-postgres 5433:5432 &
sec() { kubectl -n staging get secret "$1" -o jsonpath="{.data.$2}" | base64 -d; }
export DATABASE_URL="postgresql://playpalz:$(sec database-credentials DB_PASSWORD)@localhost:5433/playpalz"
# The seed loads apps/api/.env for Spaces and Mux; set staging's first, or it uploads to your local bucket.
export SPACES_BUCKET="$(sec spaces-credentials S3_BUCKET)" SPACES_ACCESS_KEY="$(sec spaces-credentials AWS_ACCESS_KEY_ID)" \
  SPACES_SECRET_KEY="$(sec spaces-credentials AWS_SECRET_ACCESS_KEY)" SPACES_REGION="$(sec spaces-credentials AWS_REGION)" \
  SPACES_ENDPOINT="$(sec spaces-credentials S3_ENDPOINT)" SPACES_URL="$(sec spaces-credentials S3_URL)" \
  PUBLIC_CDN_URL="" MUX_ACCESS_TOKEN="" MUX_SECRET_KEY=""      # Mux off until staging has its own
cd packages/database && pnpm db:seed

The seed needs the games catalog first: igdb-heartbeat loads 2,000 games on its first start in an empty database (watch its log for game sync complete). Seeded accounts all use Password123!; see Migrations → Seeded credentials.

To start over: helm uninstall playpalz-platform -n staging, delete the data-playpalz-postgres-0 PVC, then redeploy platform and api (whose hook re-runs every migration).

Mobile environments ​

Independent of the backend deployment, driven by EXPO_PUBLIC_ENV. Values live in the EAS environment named after each profile:

ProfileEnvAPIRealtime
developmentdevelopmentlocalhost:4000localhost:4010
previewstagingapi.playpalz.gg ⚠️socket.playpalz.gg ⚠️
productionproductionapi.playpalz.ggsocket.playpalz.gg

⚠️ Preview builds still talk to production; the staging backend exists, but the preview EAS environment hasn't been switched to it yet (see the warning at the top). See Builds & Releases.

Internal documentation — PlayPalz platform