For AI agents: fetch the complete documentation index at /llms.txt, the full documentation corpus at /llms-full.txt, the agent skill at /skill.md, or this page as Markdown at /docs/web/deploy-google-cloud.md (or send Accept: text/markdown).
Deploy to Google Cloud
Build the kit's Docker image on Cloud Build, run it on Cloud Run with managed secrets, and put it on your domain.
Beginner walkthrough
Prerequisites: the app working locally, a real database you can reach from the internet (Set up database), and the provider accounts for the features you ship. You need a Google account; billing setup asks for a card even though light usage stays inside the free tier.
Quick terms: a container image is a packaged copy of your app that any host can run. Artifact Registry is Google's store for images. Cloud Build builds the image on Google's machines — no local Docker needed. Cloud Run runs the image as an HTTPS service and scales it. Secret Manager holds passwords and keys. A revision is one deployed version of the service.
What you are building: a Google Cloud project, an Artifact Registry repository holding your image, a Cloud Run service serving the app over HTTPS, secrets in Secret Manager, and a managed Postgres. The kit's root Dockerfile already produces a standalone Next.js server listening on port 8080 — exactly what Cloud Run expects.
Honest cost note: Cloud Run's free tier covers light traffic, but Cloud SQL — Google's managed Postgres — has no free tier. A Supabase or Neon free-tier database is the cheapest way to start; this guide uses that path. Consider adding a budget alert under Billing → Budgets & alerts before experimenting.
Create the account and install the CLI
1. Open console.cloud.google.com, sign in with your Google account, and accept the terms.
2. Attach billing when prompted: Billing → Link a billing account → Create billing account. A card is required to activate the account; trial credit covers early experiments.
3. Click the project dropdown → New Project, name it, and Create — or reuse the project you made for Google sign-in; one project is fine.
4. Install the Google Cloud CLI from cloud.google.com/sdk: download the installer for your operating system, complete it, then open a new terminal so the gcloud command is available.
5. Run the two commands below. The first opens a browser to authorize the CLI; the second selects your project — use the project ID shown on the console dashboard, which is not always identical to the name.
Expected: gcloud config list shows your account and project. If gcloud is not recognized, reopen the terminal or finish the installer first.
gcloud auth login
gcloud config set project YOUR_PROJECT_IDCreate the production database
Beginner path — managed Postgres outside Google. At supabase.com: New project → choose a name, database password, and region → wait for provisioning → Project Settings → Database. Supabase offers two connection strings: the session pooler / direct connection on port 5432, and the transaction pooler on port 6543 — the 5432 string is for migrations and long-lived clients, the 6543 string is for the serverless app. At neon.tech: New project → copy the connection string it shows. Either works; keep the password portion intact.
Put the direct/session string into DATABASE_URL in the root .env — the migration tool reads the root file and needs a session connection — and in apps/web/.env.local for the local dev server. Then run the migrate command once from the repository root to create the kit's tables in the new database. The DATABASE_URL secret the deployed app uses may be the pooled 6543 string instead.
The Google-native alternative is Cloud SQL, a managed Postgres inside the same project. It has no free tier and private-IP connections require a VPC connector — treat it as a later upgrade, not a starting point.
# DATABASE_URL=<the direct/session Postgres string, port 5432> in .env + apps/web/.env.local, then:
corepack pnpm --filter @supastack/db db:migrateEnable services and push the image
Run the commands below from the repository root. They enable the four Google services this deployment uses, create a Docker repository in Artifact Registry, and submit the repository to Cloud Build — which builds the root Dockerfile remotely, so Docker Desktop is not required.
Replace YOUR_PROJECT_ID, and pick one region — us-central1 below — and reuse it for the repository and the service. Expected: the build log streams for several minutes and ends with DONE / STATUS: SUCCESS. The image then appears in the console under Artifact Registry → supastack → web.
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com secretmanager.googleapis.com
gcloud artifacts repositories create supastack --repository-format=docker --location=us-central1
gcloud builds submit --tag us-central1-docker.pkg.dev/YOUR_PROJECT_ID/supastack/web:latestStore secrets in Secret Manager
Secrets must never go into --set-env-vars or the image — both are stored in plain text and visible to anyone who can read the service configuration. Secret Manager keeps them encrypted and injects them at runtime.
1. In the console open Security → Secret Manager → Create Secret.
2. Create DATABASE_URL with the full Postgres connection string — the pooled serverless string is fine for the app — AUTH_SECRET with at least 32 random characters (openssl rand -hex 32 generates one), and CREDENTIAL_ENCRYPTION_KEY with a Base64-encoded 32-byte key (openssl rand -base64 32); it encrypts the provider keys workspaces store in the app.
3. Create one secret per provider you enabled — for example STRIPE_SECRET_KEY plus STRIPE_WEBHOOK_SECRET, RESEND_API_KEY, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_SECRET. Non-secret settings such as EMAIL_PROVIDER=resend stay plain environment variables, not secrets.
The first time a service references a secret, its runtime service account needs permission to read it — when you attach secrets through the console's service editor, Google offers that grant automatically; on the CLI the deploy error names the missing Secret Manager role to grant.
# Secrets to create in Secret Manager:
DATABASE_URL # Postgres connection string — the pooled serverless string is fine
AUTH_SECRET # openssl rand -hex 32
CREDENTIAL_ENCRYPTION_KEY # openssl rand -base64 32
# plus one per enabled provider, e.g.:
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
RESEND_API_KEY
GOOGLE_CLIENT_SECRET
GITHUB_CLIENT_SECRETDeploy the Cloud Run service
Console path: Cloud Run → Create service → Deploy one revision from an existing container image → select the pushed …/supastack/web:latest image → choose the same region → under Authentication choose Allow unauthenticated invocations (it is a public app) → Create.
The CLI equivalent below adds the environment wiring in the same step. Plain-text configuration goes in --set-env-vars; secrets are referenced by their Secret Manager name in --set-secrets. Add your provider flags (EMAIL_PROVIDER, BILLING_PROVIDER, AI_PROVIDER, JOB_PROVIDER) as plain env vars alongside APP_URL.
APP_ENV=production is required — without it the app boots in local mode: production config validation is skipped, secure cookie prefixes stay off, and the dev-only /mail-preview page remains publicly reachable (leaking the local email outbox when EMAIL_PROVIDER=local). Production then requires four more values at boot: EXPO_PUBLIC_API_URL is the public https URL the mobile app uses to reach the API — set it to the service URL (same as APP_URL for a web-only deploy); EXPO_PUBLIC_APP_SCHEME is the mobile deep-link scheme — any valid value such as supastack works when you ship no mobile app; MOBILE_ALLOWED_ORIGINS is the comma-separated list of exact https origins the server trusts — your app URL is the right start, and wildcards are rejected; CREDENTIAL_ENCRYPTION_KEY encrypts stored provider credentials and stays in Secret Manager.
Cloud Run assigns a https://….run.app URL on the first deploy, and the app's APP_URL must equal its real public URL for auth callbacks and links to work. So deploy once with the placeholder, copy the *.run.app URL from the service page, then update the service (console: Edit & deploy new revision, or rerun the command) with APP_URL — and the matching EXPO_PUBLIC_API_URL / MOBILE_ALLOWED_ORIGINS values — set to it. The container port stays 8080 — the Cloud Run default already matches the Dockerfile's PORT.
gcloud run deploy supastack --image us-central1-docker.pkg.dev/YOUR_PROJECT_ID/supastack/web:latest --region us-central1 --allow-unauthenticated --set-env-vars "APP_ENV=production,APP_URL=https://YOUR_SERVICE_URL,EXPO_PUBLIC_API_URL=https://YOUR_SERVICE_URL,EXPO_PUBLIC_APP_SCHEME=supastack,MOBILE_ALLOWED_ORIGINS=https://YOUR_SERVICE_URL" --set-secrets "DATABASE_URL=DATABASE_URL:latest,AUTH_SECRET=AUTH_SECRET:latest,CREDENTIAL_ENCRYPTION_KEY=CREDENTIAL_ENCRYPTION_KEY:latest"Map your own domain (optional)
Skip this while testing — the *.run.app URL already serves HTTPS. When you are ready: Cloud Run → your service → Custom Domains / Domain Mappings → Add mapping → pick the service and enter your domain.
Verify domain ownership if prompted (Google walks you through it), then create the DNS records the mapping screen lists at your registrar. The managed HTTPS certificate is issued automatically after the records propagate — usually minutes, up to a day.
After the domain serves traffic, update APP_URL to https://yourdomain.com and register the matching OAuth redirect URIs (https://yourdomain.com/api/auth/callback/…) — the sign-in guides cover where.
Verify the deployment
Open https://YOUR_SERVICE_URL/api/health — expect ok. Then exercise the app end to end: open the URL, sign up, request an email code, confirm it reaches the inbox (that needs EMAIL_PROVIDER=resend, RESEND_API_KEY, and a verified sender — the local adapter never reaches real inboxes), sign in, and use one feature you enabled.
Cloud Run → your service → Logs shows request and error output. The Revisions tab offers one-click rollback to the previous revision if a deploy goes wrong.
Mobile and desktop clients must point at this HTTPS backend: EXPO_PUBLIC_API_URL in apps/mobile/.env.local for Expo, SUPASTACK_APP_URL for the desktop shell.