WorkOS Production Configuration
Looking for the list of GitHub secrets? See the GitHub Secrets Inventory — single source of truth for which secrets exist, where they live, and which code reads them. This doc covers WorkOS-side config (orgs, OAuth clients, DNS); the inventory covers the secret names that flow into the deploy.
Context
This document captures the live configuration of the WorkOS production environment powering app.securityv0.com. Cutover from REQUIRE_AUTH=false dev bypass to real WorkOS prod authentication happened on 2026-04-30 (Phase B). This is an operational runbook — it describes what is, not why. The design rationale lives in 13 — Authentication and User Management; the architecture decisions are in ADR-016 and ADR-017.
⚠ Upcoming changes (per Auth Simplification Plan, 2026-05-08)
Several env vars described below are scheduled for removal or rename as part of the Auth Simplification Plan. Do not bake new automation against the current names without checking the plan's status.
| Env var | Current state | Planned change | Step |
|---|---|---|---|
STAGING_PERSONAL_AGENT_IVAN_*, PROD_PERSONAL_AGENT_IVAN_* (3 each) | Live; 6 secrets total | Removed entirely. The personal-agent bridge is being deleted. | S3 |
STAGING_WORKOS_APP_CLAUDECODE_*, PROD_WORKOS_APP_CLAUDECODE_* (2 each) | Live; 4 secrets total | Renamed to unprefixed WORKOS_APP_CLAUDECODE_CLIENT_ID/SECRET. The GitHub Environment system already scopes per-env values; the prefix is dead weight. | S1 |
WORKOS_REDIRECT_URI (legacy single-host scalar) | Live alongside WORKOS_REDIRECT_URI_ALLOWED_HOSTS | Removed. Single host expressible as one-element allowlist. | S4 |
WORKOS_COOKIE_PASSWORD | Live alongside SESSION_COOKIE_PASSWORD | Removed; consolidated to SESSION_COOKIE_PASSWORD. The split exists for the dead OIDC path. | S5 |
STAFF_SUPER_ADMIN_PROVIDER_USER_IDS (added by #816) | Live; allowlist mechanism #3 | Resolution pending in S-1 — either deleted (if A or B is chosen) or becomes canonical with the other two deleted (if C is chosen). | S-1 |
OIDC_*, JWT_*, ALLOWED_API_KEYS, API_KEY_HEADER | Listed in env.ts but unused at runtime | Removed entirely. Dead code. | S2 |
If you are operating prod and need to add a new auth env var: stop. Read Agent and M2M Authentication and §16 of architecture/13 first. The most likely answer is "use a path that already exists; do not add a fourth allowlist."
The current state below remains accurate for ops until each plan step lands. Each step's PR will update this section.
Prod environment overview
| Item | Value |
|---|---|
| WorkOS environment | Production (separate from staging) |
| AuthKit hosted UI | https://powerful-falcon-83.authkit.app |
| AuthKit staging UI (for comparison) | https://vast-balcony-35-staging.authkit.app |
| Custom OAuth domain | auth.securityv0.com |
| Application frontend | https://app.securityv0.com |
| OAuth callback | https://app.securityv0.com/auth/callback |
Tip: Check the URL bar when debugging a redirect chain.
powerful-falcon-83.authkit.app= prod;vast-balcony-35-staging.authkit.app= staging. ULID prefixes inclient_idvalues (e.g.,client_01KQ09N7...) are NOT environment indicators — confirm against the WorkOS dashboard.
WorkOS Organizations (prod)
| Organization | org_id | Purpose |
|---|---|---|
securityv0-internal | org_01KQE0B65RKR28CYAHHGEYKA1M | Super-admin org. Members get cross-tenant access per arch doc §6. |
sv0-default | org_01KQE1M2YMVNH9Y7V1XQ4NW58D | Default tenant for new evaluation users. |
Source of truth: WorkOS dashboard. The Mongo tenants collection mirrors the provider_org_id field. These org IDs are documented explicitly because Mongo seeding scripts and tenant-provisioning runbooks need them by value.
The
tenants.provider_org_idfield is unique — at most one Mongotenantsrow can claim a given WorkOS org ID. The IDOR-class rationale, thetenant_slugcookie-mint escape hatch for staff super-admin, and the trigger conditions for a future bindings-table refactor are documented in13-authentication-and-user-management.md§3.5.Environment parity: staging WorkOS has independent org IDs for the same logical orgs. The staging counterparts live in the dev GitHub Environment as
WORKOS_SUPER_ADMIN_ORG_ID(staff) and ondev.securityv0.com'stenants.default.provider_org_id(default-tenant binding). Look them up in the WorkOS dashboard's Staging environment when needed — they are intentionally not committed to this doc because they rotate on environment rebuilds.
DNS records (Cloudflare-managed)
| Record | Type | Target | Cloudflare record ID | Proxied |
|---|---|---|---|---|
auth.securityv0.com | CNAME | cname.workos-dns.com | 2b59204c9a86fd5deebf3325cdbefc37 | No (DNS-only) |
⚠️ Never proxy
auth.securityv0.comthrough Cloudflare. This breaks Google sign-in. Theproxied: false(gray cloud, DNS-only) flag must remain set on this DNS record. WorkOS provisions and renews the TLS certificate forauth.securityv0.comdirectly. If Cloudflare proxying is enabled, Cloudflare terminates TLS and WorkOS can no longer provision or renew its cert — Google sign-in via the custom domain fails with a certificate error.
Verification
# CNAME resolves to WorkOS
dig auth.securityv0.com CNAME +short
# Expected: cname.workos-dns.com.
# TLS cert is issued by WorkOS, NOT Cloudflare
echo | openssl s_client -connect auth.securityv0.com:443 -servername auth.securityv0.com 2>/dev/null \
| openssl x509 -noout -issuer
# Expected: something from Let's Encrypt / WorkOS — NOT "O=Cloudflare, Inc."
Google OAuth client (GCP Console)
The WorkOS connection ID for Google OAuth is a67he6q7eyNlXD5NL0NrXEBjQ. This is a single WorkOS connection served under both auth.workos.com and auth.securityv0.com — it is not two connections. Both redirect URI variants must be present in the GCP OAuth client to keep both paths functional.
| Redirect URI | Required for | Removable? |
|---|---|---|
https://auth.workos.com/sso/oauth/google/a67he6q7eyNlXD5NL0NrXEBjQ/callback | Original WorkOS-hosted callback — fallback if custom domain breaks | Keep — backup path |
https://auth.securityv0.com/sso/oauth/google/a67he6q7eyNlXD5NL0NrXEBjQ/callback | Custom domain — Google consent screen shows auth.securityv0.com instead of auth.workos.com | Required — branded login UX |
https://powerful-falcon-83.authkit.app/auth/google/callback | AuthKit hosted UI fallback | Keep — covers AuthKit-hosted flows |
| (staging redirect URIs) | Staging environment | Keep — do not remove |
| (forward-looking custom domain URIs already added) | Future tenant-specific domains | Keep — already provisioned |
Removing the auth.workos.com URI is safe only if the custom domain is verified-and-active in WorkOS and no AuthKit flows rely on the hosted domain. We keep it as a fallback.
WorkOS auth methods enabled (prod)
Configure in WorkOS dashboard → Authentication → Methods (per environment — staging and prod are independent).
| Method | Status | Notes |
|---|---|---|
| Magic Auth | ON | Default for evaluation users |
| Google OAuth | ON | Custom domain: auth.securityv0.com |
| Password | OFF | We do not manage passwords |
| SAML SSO | Per-customer | Enabled per org via Admin Portal on contract sign — not a global toggle |
Warning: A stock WorkOS environment starts with no methods enabled, showing only the "Continue with SSO" option. If you provision a new environment or restore from backup, re-enable Magic Auth and Google OAuth manually before testing.
Super-admin provisioning and JIT policy
Two separate mechanisms control access for SecurityV0 staff and customer-prospect users; do not confuse them.
Super-admin (SecurityV0 staff)
Super-admin status is derived in src/api/routes/auth.ts at callback time (NOT in the membership middleware). The precedence is:
- If the auth provider explicitly set
result.isSuperAdmin(e.g. CF Access JWT path; also theDevAuthProviderfor local) → use it. - Else if
WORKOS_SUPER_ADMIN_ORG_IDenv var is set → super-admin iff the user'sproviderOrgIdmatches that org. This is the prod path. - Else (env var unset — dev/test fallback only) → super-admin iff email ends with
@securityv0.com.
For prod to grant super-admin correctly:
WORKOS_SUPER_ADMIN_ORG_IDmust be set in the prod GitHub Environment toorg_01KQE0B65RKR28CYAHHGEYKA1M(thesecurityv0-internalprod org).- Each SecurityV0 staff member must be a member of
securityv0-internalin the WorkOS dashboard. A bare@securityv0.comGoogle sign-in is NOT enough on its own — without WorkOS membership, the user authenticates but lands on the "No workspace access" page. - Verify both:
gh secret list --env prod --repo SecurityV0/sv0-platform | grep SUPER_ADMIN_ORG_ID, plus dashboard → Production → Organizations → securityv0-internal → Members.
Customer-prospect JIT (verified-domain auto-join)
A separate JIT mechanism in src/api/middleware/auth-middleware.ts (membership middleware) auto-creates a member-role membership when a logged-in user's email domain matches a tenant's verified_domains list. This:
- Only applies to tenant-scoped routes (
/api/v1/tenants/:slug/...), not at callback time. - Creates
memberrole only — never super-admin. - Requires the tenant to have the email's domain explicitly added to
verified_domainsin Mongo.
Customer prospects without a verified-domain match require explicit invitation via the WorkOS Admin Portal or the scripts/provision-eval-tenant.ts script.
Cookie / session config
| Item | Value / location |
|---|---|
| Cookie name | sv0_session |
| Cookie flags | HttpOnly, Secure, SameSite=Lax |
| Session library | iron-session |
| Env variable | WORKOS_COOKIE_PASSWORD |
| Secret length | 64-char hex (32 bytes) |
| Last rotated | 2026-04-30 |
| 1Password location | securityv0-prod / WorkOS / cookie-password |
Rotation procedure
Iron-session cookies are encrypted (sealed with iron-session's AES-256 + HMAC-SHA256 scheme) using WORKOS_COOKIE_PASSWORD. Rotating this value invalidates all active sessions — every existing cookie fails to decrypt and every logged-in user must re-authenticate.
- Generate a new 32-byte secret:
openssl rand -hex 32 - Update the GitHub
productionenvironment variableWORKOS_COOKIE_PASSWORD. - Update the 1Password entry at
securityv0-prod / WorkOS / cookie-password. - Redeploy the production container (picks up the new env var).
- Verify login works end-to-end before closing the change window.
- Update the "Last rotated" date in this runbook so future rotations have an accurate baseline.
Plan rotation during a low-traffic window (e.g., off-hours weekday evening).
Common operational gotchas (hit during cutover)
"Continue with SSO" only — no Magic Auth or Google buttons
Cause: WorkOS auth methods are not enabled in the environment. A stock environment shows only the SSO option.
Fix: WorkOS dashboard → Authentication → Methods → enable Magic Auth and Google OAuth. Repeat per environment — staging and prod are independent.
redirect_uri_invalid after deploy
Cause: WorkOS rejects the OAuth callback URI because it is not in the app's allowed redirect list.
Fix: WorkOS dashboard → Configuration → Redirects → add the URI. This is at the WorkOS app level, not per-organization. Also add the URI to the GCP OAuth client if it is a new custom-domain variant.
ULID prefixes do not identify the environment
Cause: client_id like client_01KQ09N7... looks like a staging ID but could be prod.
Fix: Always confirm which environment an ID belongs to by looking it up in the WorkOS dashboard. Never infer environment from the ULID prefix.
AuthKit subdomain tells you the environment
| Subdomain pattern | Environment |
|---|---|
<name>.authkit.app (no -staging suffix) | Production |
<name>-staging.authkit.app | Staging |
When debugging a redirect chain, check the URL bar. Our prod AuthKit URL is powerful-falcon-83.authkit.app; staging is vast-balcony-35-staging.authkit.app.
Custom domain CNAME requires WorkOS verification before redirect URIs work
After creating the DNS CNAME record for auth.securityv0.com, the custom domain must be explicitly verified in WorkOS before redirect URIs using it will be accepted:
- WorkOS dashboard → Authentication → Custom Domain → click Verify DNS
- After verification passes → click Test Google Redirect URI
- Click Save
Skipping any step leaves the custom domain in a half-configured state where the CNAME resolves but WorkOS rejects redirect URIs pointing to it.
Verification commands
Copy-paste shell to confirm prod auth is healthy:
# DNS — CNAME points to WorkOS
dig auth.securityv0.com CNAME +short
# Expected: cname.workos-dns.com.
# TLS — cert is from WorkOS (not Cloudflare)
echo | openssl s_client -connect auth.securityv0.com:443 -servername auth.securityv0.com 2>/dev/null \
| openssl x509 -noout -issuer
# Expected: issuer from Let's Encrypt or WorkOS CA — NOT Cloudflare, Inc.
# AuthKit hosted UI is reachable
curl -sI https://powerful-falcon-83.authkit.app/ | head -1
# Expected: HTTP/2 307 (redirects to api.workos.com/user_management/initiate_login).
# A 200 indicates AuthKit is in an unexpected state; 4xx/5xx means WorkOS is down.
# Custom domain is NOT proxied through OUR Cloudflare zone.
#
# Header-based detection (cf-ray, `server: cloudflare`) does NOT work here:
# WorkOS itself fronts `cname.workos-dns.com` with Cloudflare, so those
# headers appear on `auth.securityv0.com` regardless of OUR proxied flag.
# Use one of the two checks below instead.
#
# Check 1 (fastest, no API token): the TLS issuer above already catches a
# wrong-direction proxied state — if our zone proxied this record, our edge
# would terminate TLS and the issuer would be Cloudflare, Inc. The Let's
# Encrypt / WorkOS CA result confirms WorkOS is the TLS terminator, which
# is only true when `proxied: false`.
#
# Check 2 (definitive): query the Cloudflare DNS API for the proxied flag
# directly. This requires a token with **Zone:Read** scope on the
# `securityv0.com` zone, which is NOT the same as the Pages-deploy
# `CLOUDFLARE_API_TOKEN` documented in `ci-cd-operations.md`. Either reuse
# the Terraform infra token (`CF_API_TOKEN_TERRAFORM_SV0_INFRA`) or mint a
# fresh Zone:Read token in the Cloudflare dashboard.
#
# ZONE_TOKEN="<Zone:Read scoped token>" # NOT CLOUDFLARE_API_TOKEN
# SV0_ZONE_ID="$(curl -sS -H "Authorization: Bearer $ZONE_TOKEN" \
# 'https://api.cloudflare.com/client/v4/zones?name=securityv0.com' \
# | jq -r '.result[0].id')"
# curl -sS -H "Authorization: Bearer $ZONE_TOKEN" \
# "https://api.cloudflare.com/client/v4/zones/$SV0_ZONE_ID/dns_records?name=auth.securityv0.com" \
# | jq '.result[] | {name, type, content, proxied}'
# Expected: { "name": "auth.securityv0.com", "type": "CNAME",
# "content": "cname.workos-dns.com", "proxied": false }
# If "proxied": true → flip to false in the Cloudflare dashboard or via the API.
# Visual confirmation (always available): Cloudflare dashboard → DNS →
# `auth.securityv0.com` row → cloud icon must be GRAY (DNS only), not orange.
# App login redirects to AuthKit
# NOTE: app.securityv0.com is also gated by Cloudflare Access. Without a CF service-token
# header pair, this curl returns the CF Access redirect, not the WorkOS redirect.
# To skip CF Access, supply the service token (see cf-access-service-token-setup.md):
# curl -sI https://app.securityv0.com/login \
# -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
# -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
# | grep -i location
# Expected with token: Location: https://powerful-falcon-83.authkit.app/...
Out of scope — what this doc does NOT cover
| Topic | Where to look |
|---|---|
| Architecture and design rationale | 13 — Authentication and User Management |
| Provider selection rationale | ADR-017 |
| Webhook receiver implementation | Deferred — not yet wired |
| SCIM Directory Sync | Per-customer, not yet enabled |
| SAML connections per customer | Created via Admin Portal on contract sign |
| Branding the hosted login page (colors, logo, font) | WorkOS AuthKit branding (in the brand spec, not here — this is design application, not auth config) |
Related
- Architecture: 13 — Authentication and User Management
- ADR-016: Multi-Tenant Authentication Architecture
- ADR-017: WorkOS as Authentication Provider
- Phase B cutover runbook (sv0-platform): in progress on branch
docs/phase-b-prod-cutover— not yet merged tomain. Captures the 2026-04-30 cutover sequence (env secrets, redirect URI registration, mongosh tenant doc updates, smoke test). - Cloudflare Zero Trust: Access Protection
- Service token setup: CF Access Service Token Setup