Skip to content

GCP project setup

Quayside’s recommended cloud deployment uses Google Cloud Run for the API, Firebase Hosting for the dashboard and docs, and an external Postgres (Neon, AWS RDS, Cloud SQL — whatever fits your shape). This page walks through the one-time GCP foundation: project, APIs, container registry, secrets, and the runtime service account.

You only run these steps once per environment (demo, staging, prod). After this, deployment is a single command — see Self-hosting.

Prerequisites

You need:

  • A Google account with billing set up
  • gcloud CLI installed (brew install google-cloud-sdk on macOS, or download)
  • Authenticated locally: gcloud auth login

Verify:

Terminal window
gcloud --version
gcloud auth list # shows your active account
gcloud billing accounts list # at least one entry, OPEN = True

If gcloud billing accounts list is empty, set up billing in the GCP console before continuing.

A. Project + billing

Project IDs are globally unique, lowercase, 6–30 chars, hyphens allowed. Pick something namespaced to your org or your name.

Terminal window
export PROJECT_ID="quayside-demo-yourname" # ← yours
export BILLING_ACCOUNT="01XXXX-XXXXXX-XXXXXX" # from `gcloud billing accounts list`
gcloud projects create "$PROJECT_ID" --name="Quayside Demo"
gcloud billing projects link "$PROJECT_ID" --billing-account="$BILLING_ACCOUNT"
gcloud config set project "$PROJECT_ID"

If reusing an existing project, skip the create and just gcloud config set project <existing>.

B. Enable required APIs

These are the GCP services Quayside touches. Enabling is free; you only pay for usage.

Terminal window
gcloud services enable \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
secretmanager.googleapis.com \
cloudscheduler.googleapis.com \
iam.googleapis.com
APIWhy
runCloud Run service — where the API container runs
artifactregistryWhere Cloud Run pulls the image from
cloudbuildBuilds the Docker image (optional — you can build locally and push)
secretmanagerHolds the DB URL, JWT secret, provider keys
cloudschedulerTriggers the nightly demo-reset job (optional)
iamService-account bindings

Takes about 30 seconds.

C. Artifact Registry repository

This is where your Quayside image lives.

Terminal window
export REGION="europe-west2" # pick a region close to your Postgres
gcloud artifacts repositories create quayside \
--repository-format=docker \
--location="$REGION" \
--description="Quayside container images"
# Configure local Docker to push to this repo
gcloud auth configure-docker "${REGION}-docker.pkg.dev"

Region pairing rules of thumb:

Your PostgresGCP region
Neon eu-west-2 (AWS London)europe-west2 (London)
Neon us-east-2 (AWS Ohio)us-east4 (Virginia) — closest
AWS RDS us-west-2us-west1 (Oregon)
Cloud SQL in us-central1same — us-central1

Cross-region adds 30–100ms of round-trip per database query and the proxy makes ~6 queries per call. Picking the right region matters more than picking the right CPU shape.

D. Secret Manager — store secrets

Three secrets go in Secret Manager:

Terminal window
# 1. Postgres connection string
echo -n 'postgresql://USER:PASS@HOST.neon.tech/DB?sslmode=require' \
| gcloud secrets create quayside-database-url --data-file=-
# 2. JWT signing secret — 32 random bytes
openssl rand -base64 32 \
| gcloud secrets create quayside-jwt-secret --data-file=-
# 3. (Optional) Anthropic API key — for real LLM forwarding
# Leave this unset and the proxy uses MockUpstream — fine for demo.
# echo -n 'sk-ant-...' \
# | gcloud secrets create quayside-anthropic-api-key --data-file=-

Verify:

Terminal window
gcloud secrets list

You should see two (or three) secrets.

E. Runtime service account

Cloud Run runs as this identity. It needs to pull images from Artifact Registry and read secrets from Secret Manager.

Terminal window
gcloud iam service-accounts create quayside-runtime \
--display-name="Quayside Cloud Run runtime"
export SA_EMAIL="quayside-runtime@${PROJECT_ID}.iam.gserviceaccount.com"
# Grant Secret Manager read access
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/secretmanager.secretAccessor"
# Grant Artifact Registry read access (same project is auto-granted; this is belt-and-braces)
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:${SA_EMAIL}" \
--role="roles/artifactregistry.reader"

Verify:

Terminal window
gcloud iam service-accounts describe "$SA_EMAIL"

You should see the display name and email.

F. Persist the config locally

The deploy script (deploy/deploy-api.sh in the repo) sources deploy/.env if it exists. Persist everything you just set up there so you don’t have to remember it next session:

Terminal window
cd <quayside-repo>
cp deploy/.env.example deploy/.env
# Edit deploy/.env and fill in:
# PROJECT_ID, BILLING_ACCOUNT, REGION, SA_EMAIL,
# NEON_DATABASE_URL (the actual DSN — for local migration runs)

deploy/.env is gitignored; deploy/.env.example is a committed template.

You’re ready

After steps A–F you have:

  • A project with billing
  • Six APIs enabled
  • An Artifact Registry repo
  • Two or three secrets in Secret Manager
  • A runtime service account with the right permissions
  • Local .env carrying everything the deploy script needs

No containers yet, no Cloud Run service, no public URL — those come from running ./deploy/deploy-api.sh (see Self-hosting for the deploy step).

Cost expectation

At demo traffic with everything scaled to zero between calls:

ServiceMonthly
Cloud Run (API, scale-to-zero)$0–3
Artifact Registry storage<$0.10
Secret Manager (10k ops free)$0
Cloud Scheduler (3 jobs free)$0
GCP subtotal~$3

Plus whatever you pay for Postgres outside GCP (Neon free tier covers a demo) and any Anthropic spend you allow.

Tearing it down

If you want to throw away the demo:

Terminal window
gcloud projects delete "$PROJECT_ID"

That deletes everything in one shot — Cloud Run service, Artifact Registry repo, Secret Manager entries, service account. The project enters a 30-day pending-deletion state where you can recover it; after 30 days it’s gone.