Infrastructure Overview¶
How the tomoda app fits into production. This section is app-perspective — what runs where, how a request reaches the backend, how secrets get into a Pod. For the infrastructure itself (Terraform, K8s manifests, Argo CD, cluster operations), see the separate devops docs.
Environments¶
Three environments. Each is intentionally a strict superset of the previous one so surprises in production are minimized.
Local¶
Everything runs on the developer's machine. Infra dependencies (Postgres, Redis, MinIO, Photon) come up via docker-compose.dev.yml; the Go backend and the Expo frontend run outside Docker for fast iteration and hot reload.
| Component | How it runs locally |
|---|---|
| Postgres + PostGIS | kartoza/postgis:15-3.3 container on :5432 |
| Redis | redis:7-alpine container on :6379 |
| Object storage | MinIO container on :9000 / :9001 (S3-compatible) |
| Geocoder | Photon container on :2322 |
| Backend (Go) | go run / Air hot reload, served on :8080 |
| Frontend (Expo) | npm run dev, served on :8081 |
task dev orchestrates this — see Docker Compose and the Runbook.
Dev (GKE)¶
A non-prod Kubernetes namespace (tomoda) on the shared GKE cluster used for integration testing and pre-prod validation. Application manifests live in the separate devops/ repo at k8s/apps/tomoda/overlays/dev/. Argo CD watches that repo and reconciles cluster state. Secrets are sourced from GCP Secret Manager via External Secrets Operator.
Prod (GKE)¶
Same Kubernetes cluster as dev, isolated in the prod namespace. Argo CD reconciles k8s/apps/tomoda/overlays/prod/.
How prod actually runs
Prod is not managed databases. Postgres runs in-cluster as a CloudNative-PG (CNPG) cluster, Redis runs in-cluster as the Bitnami Helm chart. Static assets (avatars, chat images) go to AWS S3 + CloudFront, not GCS. GCS is only used for CNPG backups and Photon indexes. For the full story, see DevOps → Architecture.
Production topology (app-perspective)¶
flowchart LR
User([User on iOS / Android / Web]) -->|HTTPS| CF[Cloudflare DNS<br/>proxied:false]
CF -.->|assets.tomoda.life| CFront[AWS CloudFront]
CFront --> S3[(S3 Bucket<br/>tomoda-assets-prod)]
CF -->|api/app/www<br/>tomoda.life| LB[GCP Load Balancer]
LB --> Traefik[Traefik Ingress<br/>in-cluster]
Traefik -->|/api, /ws| Backend[Backend Pod<br/>Go binary, :8080]
Traefik -->|/| Frontend[Frontend Pod<br/>Nginx + Expo static export, :8081]
Backend --> CNPG[(CNPG Postgres<br/>in-cluster + PostGIS)]
Backend --> Redis[(Bitnami Redis<br/>in-cluster)]
Backend -.->|presigned PUT| S3
Backend --> Photon[Photon Pod<br/>in-cluster :2322]
Backend -->|outbound| Stripe[Stripe API]
Backend -->|outbound| Resend[Resend / Email]
Backend -->|outbound| Google[Google APIs<br/>OAuth, Places]
CNPG -.->|Barman WAL+base| GCS[(GCS<br/>backups bucket)]
PhotonIndex[(GCS<br/>photon-index bucket)] -.->|FILE_URL polling| Photon
Details worth calling out:
- Two edges, not one. Static assets (
assets.tomoda.life) go through AWS CloudFront inap-northeast-1. API + web traffic (api.tomoda.life,app.tomoda.life,www.tomoda.life) goes through Cloudflare DNS → GCP Load Balancer → Traefik (in-cluster ingress). Cloudflare is DNS-only (proxied: false) — not an edge cache. - One ingress, two backends. Traefik routes
/api/v1/*and/ws/*to the Go backend Pod, everything else to the frontend Pod (Nginx serving the Expo web static export). Seek8s/apps/tomoda/base/ingress.yamlin the devops repo. - Frontend is static. The
frontend/container is a multi-stage build (Node 20 → Nginx Alpine).EXPO_PUBLIC_*env vars bake in at build time — see Cloud Build. - WebSocket Hub is single-process. The backend's WS hub holds state in memory; the Deployment runs with
replicas: 1for that reason. Scaling horizontally would require migrating presence/fanout to Redis pub/sub. Seebackend/internal/websocket/hub.go. - Photon is internal-only. The Photon Service is
ClusterIPathttp://photon.platform.svc.cluster.local:2322. No external ingress. - Outbound for third-party APIs. Stripe, Resend, Google OAuth, Google Places — all called from the backend. Webhooks come back inbound through Cloudflare → Traefik → backend.
How the app gets there¶
| Concern | App side | Infra side (devops repo) |
|---|---|---|
| Image build | cloudbuild-backend.yaml, cloudbuild-frontend.yaml |
Cloud Build, Artifact Registry |
| Deployment | n/a — GitOps via Argo CD | Argo CD, Tomoda K8s app, Deploy runbook |
| Postgres | DSN env var (DB_HOST, DB_USER, DB_PASSWORD) |
CNPG cluster, Backups |
| Redis | URL env var (REDIS_*) |
Bitnami Redis |
| Object storage | S3 SDK + IAM creds via s3-uploader-secret |
S3, CloudFront, Uploader IAM |
| Photon | PHOTON_URL env var → in-cluster service DNS |
Photon Deployment, Photon indexer |
| Secrets | envFrom: backend-secrets and s3-uploader-secret |
Secrets Management, External Secrets |
| TLS | n/a — ingress-level | cert-manager, TLS |
| DNS | n/a | Cloudflare, External-DNS |
Region & project¶
- GCP project:
development-485000(single project, dev+prod via K8s namespaces) - GKE cluster:
gke-tomodainasia-east1-a - Artifact Registry:
asia-east1-docker.pkg.dev/development-485000/tomoda-{dev,prod}-repo/ - AWS region:
ap-northeast-1(static assets only)
What's not in this section¶
- Mobile (iOS / Android) builds and OTA updates — that's the EAS pipeline. See Native Release.
- Application-layer concerns (queues, scheduler, WebSocket Hub) — see
backend/infrastructure/. - Cluster operations, disaster recovery, scaling decisions — see DevOps → Operations.