Runtime Configuration

Google Cloud Configuration

How to configure Zeeq to run on Google Cloud Platform (GCP).

Services

  • KMS
  • SQL
  • Secret Manager
  • Pub/Sub
  • Cloud Storage
  • Cloud Run
  • OAuth

Service Account Roles

RolePurpose
Cloud KMS ViewerAllows the service account to read the KMS key for decrypting secrets.
Cloud SQL ClientAllows the service account to connect to Cloud SQL instances.
Secret Manager Secret AccessorAllows the service account to access secrets stored in Secret Manager.
Pub/Sub EditorAllows the service account to publish and subscribe to Pub/Sub topics.

Google OAuth

Required to allow login with Google

SettingsValueDescription
Application typeWeb applicationSelect "Web application" as the application type.
Namezeeq-oauth-prodName your application.
Authorized JavaScript originshttps://app.zeeq.aiThe web application domain for valid origins
Authorized redirect URIshttps://app.zeeq.ai/auth/callback/googleThe 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.

Ensure that the service account is already provisioned.
# 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"
This bucket is a single shared namespace across every organization's private-source clones — paths are scoped by org+library only at the application layer (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.
Each ingest run still deletes its own workspace directory on dispose, regardless of storage medium (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.

SettingValuesPurpose
ZEEQ_RUN_MODEweb, workerSelects the host shape. web starts ASP.NET Core; worker starts the generic-host worker without HTTP middleware.
ZEEQ_MESSAGING_ROLEproducer, consumer, producer-consumerSelects which Brighter messaging services the process registers.
ZeeqMessaging__ProviderGcpPubSubSelects the production messaging transport. CI forces Postgres regardless of this setting.
ZeeqMessaging__GcpPubSub__ProjectIdGCP project idRequired when the provider is GcpPubSub; used to create and validate topics and subscriptions.
GCP_PROJECT_IDGCP project idPassed by the deployment scripts and used as a runtime fallback for Pub/Sub project resolution.

Production uses two Cloud Run runtimes:

RuntimeRequired env varsBehavior
Web serviceZEEQ_RUN_MODE=web, ZEEQ_MESSAGING_ROLE=producer, ZeeqMessaging__Provider=GcpPubSub, ZeeqMessaging__GcpPubSub__ProjectId=$GCP_PROJECT_IDAccepts HTTP traffic and publishes messages only.
Worker poolZEEQ_RUN_MODE=worker, ZEEQ_MESSAGING_ROLE=producer-consumer, ZeeqMessaging__Provider=GcpPubSub, ZeeqMessaging__GcpPubSub__ProjectId=$GCP_PROJECT_IDRuns 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:

ScriptRuntime
build/ship.shDeploys the production web service as producer.
build/ship-worker.shDeploys the production worker pool as producer-consumer.
build/send.shBuilds/pushes the image and deploys both runtimes.