Environments
Three environments: your laptop, staging, and production.
| Environment | Where | Status |
|---|---|---|
| Local | Docker Compose + pnpm dev | Working |
| Staging | DOKS staging namespace, *.staging.playpalz.gg | Live |
| Production | DOKS default namespace | Live |
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
| Service | Port | How |
|---|---|---|
| PostgreSQL | 5432 | docker compose up -d db |
| Redis | 6379 | docker compose up -d redis (password playpalz) |
| LiveKit | 7880–7882 | docker compose up -d livekit (devkey / secret) |
| api | 4000 | pnpm --filter @playpals/api dev |
| esu | 4010 | pnpm --filter @playpals/esu dev |
| ogun | — | pnpm --filter @playpals/ogun dev |
| anansi | 4005 | pnpm --filter @playpals/anansi dev |
| web | 3000 | pnpm --filter @playpals/web dev |
| docs | 5173 | pnpm --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:
./start-dev.sh # foreground, everything
./start-dev.sh -d api # detached, one serviceSlower than running natively, but eliminates environment drift between developers.
Production
Namespace default in the DOKS cluster.
| Service | Replicas | Strategy | Ingress |
|---|---|---|---|
web | 3 | RollingUpdate | www.playpalz.gg, playpalz.gg |
api | 3 | RollingUpdate | api.playpalz.gg |
esu | 2 | RollingUpdate | socket.playpalz.gg |
ogun | 2 (HPA 2–8) | RollingUpdate | — |
anansi | 1 | Recreate | — |
igdb-heartbeat | 1 | Recreate | — |
redis | 1 (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/.
| Production | Staging | |
|---|---|---|
| Namespace | default | staging |
| Web | www.playpalz.gg, playpalz.gg | staging.playpalz.gg |
| API | api.playpalz.gg | api.staging.playpalz.gg |
| Realtime | socket.playpalz.gg | socket.staging.playpalz.gg |
| Admin | admin.playpalz.gg | admin.staging.playpalz.gg |
| Docs | docs.playpalz.gg | docs.staging.playpalz.gg |
| Replicas | As above | 1 each, no autoscaling |
| Database | Managed Postgres 15 | In-cluster Postgres 15 (playpalz-postgres, 10Gi volume), TLS with a self-signed certificate |
| Redis | 5Gi, 512 MB dataset | 1Gi, 256 MB dataset |
| Media / KYC | playpalz-media-production, playpalz-kyc | playpalz-media-production-staging, playpalz-kyc-staging (own scoped keys) |
| Stripe | Live keys (once live) | Test mode |
| RevenueCat | Production webhook | Sandbox purchases, its own webhook secret |
| Mux | Production environment | Placeholder until a Mux development environment is set up: video uploads fail at Mux |
| LiveKit, IGDB | — | Shared with production |
ENV | production | staging: 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:
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>-stagingNormally 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:
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:seedThe 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:
| Profile | Env | API | Realtime |
|---|---|---|---|
development | development | localhost:4000 | localhost:4010 |
preview | staging | api.playpalz.gg ⚠️ | socket.playpalz.gg ⚠️ |
production | production | api.playpalz.gg | socket.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.
