Skip to content

Environments

There are effectively two environments today: your laptop, and production.

EnvironmentWhereStatus
LocalDocker Compose + pnpm devWorking
StagingPlanned in playpalz-staging namespaceNot deployed
ProductionDOKS default namespaceLive

There is no staging environment

apps/mobile/eas.json builds a preview profile pointing at staging-api.playpalz.gg and staging-socket.playpalz.gg. Those hosts are not in infra/terraform/dns.tf, and the staging Ingress in infra/k8s/ingress.yaml is commented out.

So a staging build has nowhere to connect, and every change goes from a laptop straight to production. Standing up staging is the highest-value infrastructure work available, and there is already a plan for it.

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

The staging plan

dev-notes/helm-chart-staging-production-plan.md proposes converting the raw manifests into one Helm chart deployed twice into the same cluster:

infra/helm/playpalz/
├── Chart.yaml
├── values.yaml                 shared defaults, production-shaped
├── values-staging.yaml
├── values-production.yaml
├── templates/                  one directory per service
└── sealed-secrets/
    ├── staging/                re-sealed for the staging namespace
    └── production/
ProductionStaging
Namespacedefaultplaypalz-staging
Hosts*.playpalz.ggstaging.playpalz.gg, api-staging.playpalz.gg
ReplicasAs above1 each
ResourcesFullReduced

Sealed secrets stay outside templates/ and are applied with kubectl apply — they are encrypted blobs scoped to a specific namespace and secret name, so Helm should not manage their lifecycle, and each one needs a one-time kubeseal re-seal for the staging namespace.

Prerequisites for staging

  1. DNS records for staging.playpalz.gg and api-staging.playpalz.gg in dns.tf.
  2. A separate PostgreSQL database (a second digitalocean_database_db on the same cluster is the cheap option).
  3. A separate Redis, or a separate database index on the existing one.
  4. Sealed secrets re-sealed for playpalz-staging.
  5. Uncomment and template the staging Ingress.
  6. Point the mobile preview profile at the new hosts.

Reduced replicas and a shared database cluster keep the added cost to roughly $30–50/month.

Mobile environments

Independent of the backend deployment, driven by EXPO_PUBLIC_ENV:

ProfileEnvAPIRealtime
developmentdevelopmentlocalhost:4000localhost:4010
previewstagingstaging-api.playpalz.gg ⚠️staging-socket.playpalz.gg ⚠️
productionproductionapi.playpalz.ggsocket.playpalz.gg

⚠️ Those hosts do not resolve. See Builds & Releases.

Known inconsistency between manifests

ogun-deployment.yaml still reads a mux-credentials secret with TOKEN_ID / TOKEN_SECRET keys, while the sealed secret in infra/k8s/sealed-secrets/mux-secrets.yaml uses MUX_ACCESS_TOKEN / MUX_SECRET_KEY — the names api-deployment.yaml consumes. Reconcile these onto one secret when porting to Helm.

Internal documentation — PlayPalz platform