0007 - Adopt GCP as the Managed Backend Platform
Status: Accepted Date: 2026-08-07 Amended: 2026-08-10 - production Cloud SQL now starts at MVP launch rather than running from creation. See "Production database activation" below. Author: Manuel Nucci
Context
Feel Pro Club currently deploys the web application to Vercel, the NestJS API to Railway, and uses Supabase for PostgreSQL, authentication, and object storage. The MVP needs a backend platform that:
- can scale API compute to zero while idle;
- can remain below an ideal combined development and production ceiling of USD 20 per month after promotional credits expire;
- keeps the database private and authorizes access with workload identity;
- avoids operating ECS, EKS, GKE, or another cluster;
- preserves standard containers, PostgreSQL, and provider seams where they materially reduce application lock-in;
- supports an incremental migration without production data migration because current data is disposable MVP data.
AWS Lambda can scale to zero, but adapting the current NestJS HTTP runtime and privately reaching RDS without a permanently billed NAT path adds complexity and cost. Azure Container Apps is viable, but GCP provides the simplest fit for the team's experience, expected startup credits, private Cloud SQL connectivity, and Cloud Run scale-to-zero behavior.
Decision
Adopt GCP for the backend platform while keeping the web application on Vercel.
- Use separate
fpc-devandfpc-prodGCP projects inus-central1under one billing account. - Deploy the API as an OCI container to Cloud Run with request-based billing and zero minimum instances initially.
- Keep a Terraform-controlled option to set the production minimum to one only if measured cold-start latency exceeds the ten-second target.
- Let the Vercel application call Cloud Run's stable public
run.appHTTPS endpoint directly during the MVP. - Defer
api.feelproclub.com, an external Application Load Balancer, Cloud Armor, and a branded API hostname until cost or security requirements justify them. - Run PostgreSQL on a zonal, non-HA Cloud SQL instance with private IP only, daily backups, and seven-day retention.
- Connect Cloud Run to the VPC with Direct VPC egress and connect to Cloud SQL through a Cloud SQL Auth Proxy sidecar using automatic IAM database authentication.
- Give the runtime and migration workloads separate service accounts and database users. The runtime identity receives application DML permissions; the migration identity receives the required DDL permissions.
- Use Cloud Storage with a public-assets bucket for immutable team logos and a private-media bucket for profile pictures served through short-lived signed URLs.
- Store provider-neutral object keys in PostgreSQL rather than provider URLs.
- Move periodic reminder work from the in-process NestJS scheduler to an idempotent Cloud Run Job invoked by Cloud Scheduler before enabling production scale-to-zero.
- Use Artifact Registry for images and Secret Manager for secret containers and runtime access.
- Keep secret payloads out of Terraform state; Terraform manages secret resources and IAM, while an authorized operator or deployment workflow supplies values.
- Start production Cloud SQL at MVP launch, not at creation. Cloud SQL cannot be created in a stopped state (a provider limitation), so production is created running, stopped immediately as an operational step, and started again as an explicit launch step; from launch onward it stays running. Allow development Cloud SQL to be stopped operationally without Terraform attempting to reverse that daily lifecycle state.
Production database activation
Amended 2026-08-10, during Phase 2 of the migration. As originally accepted, this ADR said to keep production Cloud SQL running. Production serves no traffic until MVP launch, and the database is the only component of this platform that cannot scale to zero, so running it from creation would have meant paying the entire fixed cost floor for an environment nobody could reach. Public list pricing put the originally chosen db-g1-small near USD 26/month, above this ADR's own ideal USD 20 combined ceiling; production now uses db-f1-micro.
Cloud SQL cannot actually be created in a stopped state — the Google provider rejects activation_policy = "NEVER" at creation with "This operation is not valid for this instance" (a known limitation, hashicorp/terraform-provider-google#8874), which only a real apply against feelproclub-dev surfaced. Production is therefore created running and stopped immediately afterward as an operational step (by hand with gcloud, since infra/scripts/db-power.sh refuses to touch prod), not created stopped by Terraform. The cost and backup-coverage consequences below are unchanged by this correction: pre-launch production ends up cold either way, just one operational step later than originally described.
This ADR was amended in place rather than superseded by a new one, at the owner's direction. That is a deliberate exception to the immutability convention in docs/CLAUDE.md, recorded here so the change is visible rather than silent. The rest of this ADR is unaffected.
Consequences
Positive
- API compute can reach zero cost while idle within Cloud Run's billing model and free allowance.
- The database has no public network path and uses short-lived workload identity instead of static database credentials.
- Containers, PostgreSQL, Prisma, and provider-neutral object keys preserve the most valuable application portability.
- Development and production have separate IAM, state, network, and data blast radii.
- Vercel remains unchanged, avoiding an unnecessary frontend migration.
- Cloud Run Jobs make scheduled work reliable even when the HTTP service has zero instances.
Negative
- Cloud SQL creates a fixed monthly floor from MVP launch onward and cannot scale to zero in production.
- A stopped Cloud SQL instance takes no automated backups. Production therefore has no backup coverage between creation and launch, and backups begin at the moment it is started rather than retroactively.
- A stopped instance still bills for its provisioned disk, so "stopped" means cheap, not free.
- The first request after an idle period can experience Cloud Run startup latency.
- The MVP API hostname is a Google-managed
run.appURL rather thanapi.feelproclub.com. - Allowing direct browser invocation makes the Cloud Run endpoint public at the platform layer; application authentication, authorization, CORS, rate limiting, and safe error handling become critical.
- Cloud SQL Auth Proxy sidecars and IAM database users add deployment configuration that does not exist locally.
- Without a load balancer, the API does not have Cloud Armor or custom edge routing.
Mitigations
- Warm the API as soon as the app opens through a rate-limited endpoint that establishes a cheap database connection.
- Measure cold-start percentiles before paying for a minimum production instance.
- Keep separate liveness, readiness, and warm-up interfaces so process health is not confused with database readiness.
- Enforce exact CORS origins and WorkOS JWT verification on every protected request; treat UI authorization as convenience only.
- Use the proxy only as a deployment adapter; Prisma continues to use a standard PostgreSQL connection interface.
- Add budget alerts and review actual Cloud Run, Cloud SQL, Storage, logging, and network spend during every migration phase.
- Make starting production Cloud SQL an explicit, checklisted step of the launch promotion, and verify the first automated backup completes before treating the database as protected.
- Reconsider a load balancer and
api.feelproclub.comafter the MVP validates traffic, security, and cost requirements.