The app deploys as one Cloud Run service that serves the SPA, the REST API, and the WebSocket connection (same-origin — no proxy/CORS in prod), wired to:
- Cloud SQL (Postgres + pgvector) — durable data, via Cloud Run's native connector.
- Memorystore (Redis) — pub/sub + per-session lock, via a Serverless VPC connector.
- Cloud Storage — a bucket mounted for generated images.
- Vertex AI (Gemini + Imagen) — via the service's own identity (ADC), no key files.
The GCP infrastructure is Terraform (infra/); the app is built and
deployed by GitHub Actions (.github/workflows/deploy.yml)
using Workload Identity Federation (no service-account keys).
Scaling. Live session state lives in Redis (shared) with a per-session lock, so the service is safe to scale out: the workflow runs
--min-instances=1(warm) and--max-instances=5(scales under load). Postgres is the durable copy.
Everything in GCP — Cloud SQL, Memorystore + VPC connector, the image bucket,
Artifact Registry, both service accounts + IAM, and Workload Identity Federation —
is declared in infra/. One-time:
cd infra
cp terraform.tfvars.example terraform.tfvars # set project + github_repo
gcloud auth application-default login # your account (Owner/Editor)
terraform init && terraform applySee infra/README.md for details (billing + a default VPC are
prerequisites).
Repo → Settings → Secrets and variables → Actions. Output names == secret names:
cd infra
for k in GCP_PROJECT WIF_PROVIDER DEPLOY_SERVICE_ACCOUNT RUNTIME_SERVICE_ACCOUNT \
CLOUDSQL_INSTANCE VPC_CONNECTOR IMAGE_BUCKET REDIS_URL; do
gh secret set "$k" -b "$(terraform output -raw $k)"
done
gh secret set DATABASE_URL -b "$(terraform output -raw DATABASE_URL)"(Or copy them in by hand from terraform output.)
| Secret | Source |
|---|---|
GCP_PROJECT, WIF_PROVIDER, DEPLOY_SERVICE_ACCOUNT, RUNTIME_SERVICE_ACCOUNT |
terraform output |
CLOUDSQL_INSTANCE, VPC_CONNECTOR, IMAGE_BUCKET |
terraform output |
DATABASE_URL, REDIS_URL |
terraform output (DATABASE_URL is sensitive) |
git push origin main # or: Actions tab → "Deploy to Cloud Run" → Run workflowThe workflow runs tests, builds the image, pushes it to Artifact Registry, deploys to Cloud Run, and prints the service URL.
- pgvector is created by the app on startup (
CREATE EXTENSION IF NOT EXISTS vector); Cloud SQL grants API-created userscloudsqlsuperuser, which can create it — no manual SQL step. - Region. Everything runs in europe-west2 (London) — infra, the Gemini DM,
Imagen, and the semantic-recall embeddings (
text-embedding-004, §6) are all served there; the embedder inheritsGCP_LOCATION, so no extra env is needed (override withGEMINI_EMBED_MODELif desired). The one exception is the image-consistency modelgemini-2.5-flash-image, which isn't offered in europe-west2, so those reference-conditioned calls go to europe-west1 viaGEMINI_EDIT_LOCATION(set in the workflow). Both are EU regions. - Scaling. Live session state is shared via Redis with a per-session lock, so
instances can scale horizontally (WebSocket fan-out is pub/sub; each turn loads
the current state under the lock).
--max-instancesis set to 5 — raise as needed. - Cost. Cloud SQL
db-f1-micro+ Memorystore Basic 1 GB + the VPC connector are the ongoing baseline; Vertex (Gemini/Imagen) is per-call. To drop Redis, removeinfra/redis.tfand theVPC_CONNECTOR/REDIS_URLusage in the workflow. - Secrets hardening.
DATABASE_URL/REDIS_URLare passed as env vars; for stricter setups move them to Secret Manager and use--set-secrets.