Google Cloud Configuration
Services
- KMS
- SQL
- Secret Manager
- Pub/Sub
- Cloud Storage
- Cloud Run
- OAuth
Service Account Roles
| Role | Purpose |
|---|---|
| Cloud KMS Viewer | Allows the service account to read the KMS key for decrypting secrets. |
| Cloud SQL Client | Allows the service account to connect to Cloud SQL instances. |
| Secret Manager Secret Accessor | Allows the service account to access secrets stored in Secret Manager. |
| Pub/Sub Editor | Allows the service account to publish and subscribe to Pub/Sub topics. |
Google OAuth
Required to allow login with Google
| Settings | Value | Description |
|---|---|---|
| Application type | Web application | Select "Web application" as the application type. |
| Name | zeeq-oauth-prod | Name your application. |
| Authorized JavaScript origins | https://app.zeeq.ai | The web application domain for valid origins |
| Authorized redirect URIs | https://app.zeeq.ai/auth/callback/google | The post-authorization redirect target |
Secrets Required
Several secrets are required for OpenIddict to generate secure tokens.
# Generate the certificates used for signing the tokens
./build/certs/gen-openiddict-certs.sh
# Create the secrets in GCP Secret Manager (use the cert password)
ZEEQ_OPENIDDICT_ENCRYPTION_PASSWORD="..." ZEEQ_OPENIDDICT_SIGNING_PASSWORD="..." ./build/certs/upload-openiddict-secrets.sh
Additional secrets:
# The GitHub App Private Key (PEM); used to access GitHub API as app.
printf "SECRET_VALUE" | gcloud secrets create AppSettings__GitHub__PrivateKeyPem \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# The GitHub App Webhook Secret (used to validate the incoming webhook requests)
printf "SECRET_VALUE" | gcloud secrets create AppSettings__GitHub__WebhookSecret \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# Used when encrypting the contents of the review request link.
openssl rand -base64 48 | tr -d '\n' | \
gcloud secrets create AppSettings__CodeReview__ReviewRequestLinkEncryptionKey \
--replication-policy=automatic \
--data-file=- \
--project=$GCP_PROJECT_ID
# Used to sign Zeeq library export packages before they can be imported.
openssl rand -base64 48 | tr -d '\n' | \
gcloud secrets create AppSettings__Documents__LibraryExportSigningKey \
--replication-policy=automatic \
--data-file=- \
--project=$GCP_PROJECT_ID
# System-admin allow-list entry in provider:subject format. Create as empty and
# Then update after the first accounts are created.
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Platform__SystemAdminSubjects__0 \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# Default Fireworks API Key for the LLM model. The default model is Fireworks GLM 5.2
# (accounts/fireworks/models/glm-5p2). High and Max tiers inherit the Fast key when blank.
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Llm__Models__Fast__ApiKey \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# Fireworks API key for snippet embeddings (separate from the chat-completion key —
# different model, and rotated independently since only the sweep and the search API use it).
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Llm__Embeddings__ApiKey \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# Auth client secret for Google OAuth (login)
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Auth__Providers__0__ClientSecret \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# Auth client secret for GitHub OAuth (login)
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Auth__Providers__1__ClientSecret \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# The database connection string for the production database
# Host=/cloudsql/zeeq-ai-prod:us-central1:zeeq-pg-prod;Database=zeeq;Username=zeeq-app;Password=<password>;SSL Mode=Prefer;Pooling=true;Maximum Pool Size=20;Include Error Detail=true
printf "SECRET_VALUE" | gcloud secrets create AppSettings__Database__ConnectionString \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# OTEL API key (e.g. Datadog, Honeycomb) injected as header. e.g.: `x-honeycomb-team=abc1234...`
printf "SECRET_VALUE" | gcloud secrets create zeeq-otel-api-key-header \
--replication-policy="automatic" \
--data-file=- \
--project=$GCP_PROJECT_ID
# (Updating a secret)
printf "SECRET_VALUE" | gcloud secrets versions add AppSettings__Llm__Models__Fast__ApiKey \
--data-file=- \
--project=$GCP_PROJECT_ID
KMS Setup
KMS is used to manage encryption and decryption of user-entered API keys and secrets.
This ensures that secrets are securely encrypted at rest and can be rotated as needed.
# GCP_RUNTIME_SA=... ./build/init/gcp-kms.sh
PROJECT=$GCP_PROJECT_ID # Set in .config/mise.toml
RUNTIME_SA=$GCP_RUNTIME_SA # Set via env var
REGION=us-central1
KEYRING=zeeq-runtime
KEY=zeeq-llm-secrets
gcloud services enable cloudkms.googleapis.com \
--project="$PROJECT"
gcloud kms keyrings create "$KEYRING" \
--location="$REGION" \
--project="$PROJECT"
gcloud kms keys create "$KEY" \
--keyring="$KEYRING" \
--location="$REGION" \
--purpose=encryption \
--rotation-period=90d \
--next-rotation-time="$(date -u -v+90d '+%Y-%m-%dT%H:%M:%SZ')" \
--project="$PROJECT"
gcloud kms keys add-iam-policy-binding "$KEY" \
--keyring="$KEYRING" \
--location="$REGION" \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/cloudkms.cryptoKeyEncrypterDecrypter" \
--project="$PROJECT"
Cloud SQL
Any CloudSQL Postgres configuration is suitable.
Two flags need to be enabled:
cloudsql.enable_pg_cron=on
cron.database_name=zeeq
These flags enable pg_partman cleanup and self provisioning of partitions to work.
# Enable pg_cron extension.
PROJECT=zeeq-ai-prod
INSTANCE=zeeq-pg-prod
gcloud sql instances patch "$INSTANCE" \
--project="$PROJECT" \
--database-flags=cloudsql.enable_pg_cron=on,cron.database_name=zeeq
# Access
brew install cloud-sql-proxy
gcloud auth application-default login
gcloud auth login
gcloud services enable sqladmin.googleapis.com
cloud-sql-proxy \
--port 55432 \
zeeq-ai-prod:us-central1:zeeq-pg-prod
Ingest Workspace Storage
The worker pool clones repositories for repository content ingest under
AppSettings__Ingest__ContentRootPath — a GCS bucket mounted via Cloud
Storage FUSE, rather than the OS temp directory local dev uses by default.
LocalTempWorkspaceProvider needs no code change for this: it already reads
its workspace root from that setting.
# One-time bucket provisioning (./build/init/gcp-storage.sh)
PROJECT=$GCP_PROJECT_ID # Set in .config/mise.toml
RUNTIME_SA=$GCP_RUNTIME_SA # Set via env var
REGION=us-central1
BUCKET="${PROJECT}-ingest"
gcloud services enable storage.googleapis.com --project="$PROJECT"
gcloud storage buckets create "gs://${BUCKET}" \
--project="$PROJECT" \
--location="$REGION" \
--uniform-bucket-level-access
# 3-day lifecycle TTL as a backstop against a workspace surviving a hard
# process kill mid-run (see build/init/gcp-storage.sh for the full rule).
gcloud storage buckets update "gs://${BUCKET}" --lifecycle-file=lifecycle.json
# roles/storage.objectUser is Google's own documented role for a read-write
# Cloud Storage volume mount — see the "Grant access" section of
# https://docs.cloud.google.com/run/docs/configuring/services/cloud-storage-volume-mounts.
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET}" \
--member="serviceAccount:${RUNTIME_SA}" \
--role="roles/storage.objectUser"
LocalTempWorkspaceProvider.ResolveWorkspacePath), not by
GCS IAM, which has no native per-prefix authorization.build/ship-worker.sh mounts this bucket at /mnt/ingest and points
AppSettings__Ingest__ContentRootPath there:
--add-volume="name=ingest,type=cloud-storage,bucket=${ZEEQ_INGEST_BUCKET}" \
--add-volume-mount="volume=ingest,mount-path=${ZEEQ_INGEST_MOUNT_PATH}"
Only the worker pool mounts this bucket — the web service never runs the
ingest pipeline, so ship.sh is unaffected.
ZeeqIngestWorkspaceStartupCheck (a hosted service registered only by
ZeeqWorkerHost, before message-consumer registration) runs a synthetic
write/read/delete against the configured Ingest.ContentRootPath before the
worker starts consuming any messages — a broken mount (wrong path, missing
volume, an IAM permission gap) fails loud at startup and crash-loops the
Cloud Run worker instance.LocalIngestWorkspace's local-temp semantics
are unchanged) — a repeat sync re-clones rather than reuses a prior pull.Diagnostic Logging
# Tail the logs of the Cloud Run web service
gcloud run services logs tail zeeq-runtime \
--project zeeq-ai-prod \
--region us-central1
# Tail the logs of the Cloud Run worker
gcloud run worker-pools logs tail zeeq-worker \
--project zeeq-ai-prod \
--region us-central1
Cloud Run Runtime Roles
Zeeq uses the same container image for the HTTP web runtime and the background worker runtime. The process behavior is selected with environment variables. ZEEQ_MESSAGING_ROLE is required so a production web service cannot accidentally start consumers when the role is omitted. Production Cloud Run deployments also run with the GCP Pub/Sub transport, so the Pub/Sub project id must be available before startup topology reconciliation runs. The deployment scripts pass the explicit ZeeqMessaging__GcpPubSub__ProjectId setting from GCP_PROJECT_ID and also set GCP_PROJECT_ID for fallback resolution.
| Setting | Values | Purpose |
|---|---|---|
ZEEQ_RUN_MODE | web, worker | Selects the host shape. web starts ASP.NET Core; worker starts the generic-host worker without HTTP middleware. |
ZEEQ_MESSAGING_ROLE | producer, consumer, producer-consumer | Selects which Brighter messaging services the process registers. |
ZeeqMessaging__Provider | GcpPubSub | Selects the production messaging transport. CI forces Postgres regardless of this setting. |
ZeeqMessaging__GcpPubSub__ProjectId | GCP project id | Required when the provider is GcpPubSub; used to create and validate topics and subscriptions. |
GCP_PROJECT_ID | GCP project id | Passed by the deployment scripts and used as a runtime fallback for Pub/Sub project resolution. |
Production uses two Cloud Run runtimes:
| Runtime | Required env vars | Behavior |
|---|---|---|
| Web service | ZEEQ_RUN_MODE=web, ZEEQ_MESSAGING_ROLE=producer, ZeeqMessaging__Provider=GcpPubSub, ZeeqMessaging__GcpPubSub__ProjectId=$GCP_PROJECT_ID | Accepts HTTP traffic and publishes messages only. |
| Worker pool | ZEEQ_RUN_MODE=worker, ZEEQ_MESSAGING_ROLE=producer-consumer, ZeeqMessaging__Provider=GcpPubSub, ZeeqMessaging__GcpPubSub__ProjectId=$GCP_PROJECT_ID | Runs message consumers and can publish follow-up messages from handlers. |
Only the web service mounts the OpenIddict certificate .pfx secrets as files. Cloud Run worker pools do not support Secret Manager volume mounts, and worker mode exits before ASP.NET Core/OpenIddict setup.
The local Aspire app uses ZEEQ_MESSAGING_ROLE=producer-consumer for the single-process development shape. When ZEEQ_ASPIRE_MODE=split, the web resource switches to producer and the worker resource uses producer-consumer.
GCP_PROJECT_ID defaults to zeeq-ai-prod in build/ship.sh and build/ship-worker.sh when it is not set by the caller. Set GCP_PROJECT_ID explicitly before running build/send.sh to deploy the same image to another project.
Deployment scripts:
| Script | Runtime |
|---|---|
build/ship.sh | Deploys the production web service as producer. |
build/ship-worker.sh | Deploys the production worker pool as producer-consumer. |
build/send.sh | Builds/pushes the image and deploys both runtimes. |