Topology
A small production topology has:- a public Cloud Run service that serves the compiled console and API
- a Cloud Run worker pool for the continuous runtime worker
- a managed PostgreSQL instance shared by both application processes
api entrypoint and the worker pool runs its
worker entrypoint. Both workloads need the same DATABASE_URL and
OAO_CREDENTIAL_ENCRYPTION_KEY. Do not give the worker a public endpoint. The
worker delivers event webhooks, so give it
outbound HTTPS access to their endpoints.
Build and deliver an image
Create an Artifact Registry Docker repository in your own project and authenticate the Google Cloud CLI as an identity with permission to push to that repository. Configure Docker authentication before building and publishing. Replace each placeholder with your own value:VITE_OAO_API_MODE is a console build setting; changing it on a running
container does not rebuild the console. See the Google Cloud
Docker authentication guide
for credential-helper setup.
Resolve the published tag to an immutable digest:
sha256:<IMAGE_DIGEST>. Use that digest in the deployment
image reference:
pnpm check,
pnpm test:postgres:fresh, and a local Docker build without publishing or
deploying an image. Application versioning is also separate from infrastructure
delivery; see Releases.
Runtime configuration
Store production secrets in a secret manager and grant each workload access only to the values it needs. Configure these shared settings on both the API and worker; all example values are placeholders:api command for the public service and
the worker command for the worker pool. Migrations run idempotently when
each process starts, using DATABASE_URL. Its database principal must own the
OAO database and have CREATEROLE, permission to create pgcrypto and schema
objects, and permission to manage role memberships, object grants, and function
ownership. An ordinary application-only database login cannot run these
startup migrations.
The bundled migrations support a non-superuser database owner on Cloud SQL.
They create restricted no-login roles, including oao_app, which application
transactions assume after migration. Do not grant PostgreSQL superuser or
BYPASSRLS solely for OAO; verify the required ownership and role-management
permissions with your PostgreSQL provider.
For a full explanation of local configuration and disposable PostgreSQL checks,
see Local development. The
Railway guide has the same application process
layout on another managed platform.
Authentication
Choose one hosted authentication provider and configure its settings only on the API service. The worker does not needAUTH_PROVIDER, WorkOS credentials,
or IAP audience and identity settings.
For WorkOS, set AUTH_PROVIDER=workos and provide the WorkOS configuration
and callback URL for https://<PUBLIC_HOSTNAME>/v1/auth/callback. Provision
an existing OAO principal and membership before the first hosted login.
For Google IAP, set AUTH_PROVIDER=iap and use an exact, target-specific
audience:
IAP first-login onboarding
When the API starts withAUTH_PROVIDER=iap against an empty database, it
creates Default organization and Default project with generated IDs;
PostgreSQL stores the default selection and a one-time ownership marker. Other
authentication providers never create these defaults. No setup screen,
bootstrap command, or tenant environment variable is needed.
The first verified human login becomes the organization owner. Later verified
humans join the default project as members with explicit developer scopes and
no wildcard, tenant-administration, audit, MCP-configuration, or
credential-write permissions. IAP is the admission allowlist, so restrict it to
trusted people before the first login. Service accounts use scoped API keys and
cannot claim ownership. Membership, identity, ownership claim, and audit entry
commit together, so concurrent first logins create exactly one owner. Removing
an owner or restarting never reopens the claim.
New projects initially include only their creator. An Owner or Admin can use
the project’s Members page to add an existing IAP user by their verified
email after that user has signed in once. Remove from project keeps the
user’s organization access, while Remove organization access is the
organization-wide offboarding action. The default project’s membership cannot
be removed, and the default project cannot be deleted, because it anchors IAP
console authentication.
The default image discovers the authentication provider from the API at
runtime; an unset or empty build-time AUTH_PROVIDER does not select
development login. A console bundle built for a different provider cannot be
repaired by changing the runtime environment: rebuild and release the image.
The IAP migrations support a non-superuser database owner with CREATEROLE.
They temporarily grant schema CREATE to the oao_auth function-owner role
during ownership transfer and revoke it in the same migration transaction.
Upgrading from IAP tenant variables
Earlier releases requiredIAP_ORGANIZATION_ID and IAP_PROJECT_ID. Both are
now ignored. An existing installation reuses its single IAP tenant link for
the configured audience and keeps its IDs, names, users, and roles.
Deploy the new release while the old variables are still set, verify that
existing owners, members, and API-key callers resolve to the same tenant, then
remove the variables. Rolling back to an older release requires restoring them;
keep the database defaults and ownership marker.
If startup reports that operator review is required, the installation has a
missing or ambiguous tenant link, or an ownerless tenant. Use a reviewed
migration credential to inspect oao.iap_default_tenant,
oao.auth_tenant_links, and the organization and project memberships. Match the
tenant link to the exact IAP audience and restore an authorized owner’s
membership; never reset the ownership marker to let an arbitrary login become
owner. The provision:iap command can still link a reviewed existing principal
to its IAP identity. It does not grant membership, replace a bound identity, or
override a conflicting email.
Verify
After deployment, confirm that:- The public service returns
200from/readyzand serves the console. - The worker pool can access PostgreSQL and reaches its private
/readyzcheck. - The selected authentication flow returns to the public service.
- Both workloads run the intended immutable image digest.

