Provider signup and parity across the deployments

The infrastructure command can create GCP resources, but it cannot accept vendor contracts, prove domain ownership, choose paid plans, create globally owned GitHub organizations, or grant Workspace-wide authority. Those are explicit operator gates. One deployment is one provider attachment; the same key names appear in every row, but credentials, webhook secrets, signing keys, sender identities, Drive roots, Restate journals, and GitHub organizations never cross rows.

DeploymentGoogle browser callbackGitHub organization
neon-law-stghttps://www.neonlaw.com/auth/callbackneon-law
neon-law-prodhttps://www.neonlaw.com/auth/callbackneon-law-foundation
neon-law-prodhttps://www.neonlaw.com/auth/callbackneon-law

The three GitHub organizations use GitHub Free, the engineering contact mailbox recorded in docs/provider-environment-parity.md, and Shook Law PLLC as the controlling business. The bootstrap creates no additional invitations: the authenticated operator remains the sole initial owner until an explicit access review adds another person.

All three organizations and private Apps were created on GitHub Free with those exact slugs. Each App is installed only on the organization in its row, selects all current and future repositories, and grants repository Administration and Contents read/write. Webhook delivery is disabled. Each deployment's config.toml contains that row's App ID and installation ID, and its secrets.enc.yaml the distinct private key. The complete value-by-value worksheet lives in docs/provider-environment-parity.md.

ConfigGitHub App
neon-law-stgnavigator-neon-law-stg
neon-law-prodnavigator-neon-law-prod
neon-law-prodnavigator-neon-law-prod

Updating a provider credential

Rotation is two-sided and ordered: at the provider first, in the repository second. Re-encrypting alone revokes nothing — anyone holding repository history and the KMS key can still read every prior ciphertext, so the old value stays valid until the provider stops honouring it.

Two of these credentials cannot be re-read once created, which makes "rotate" mean replace, not look up:

  • Google OAuth client secret. Google removed retrieval entirely; the console shows only a masked suffix. Replace it with Add secret, capture the value from the one-time dialog, then delete the old secret.
  • GitHub App private key. GitHub hands over the .pem once, at generation. Replace it by generating a new key, then deleting the superseded one.

Capture the value at the moment it is shown. A secret minted without capturing it is not recoverable — it is only deletable, and it leaves a second live credential behind until you remove it.

Write the new value straight into the deployment's encrypted file. sops reads its key from the .sops.yaml creation rule for that path, encrypts per value on save, and never writes plaintext to disk:


sops deployments/neon-law-stg/secrets.enc.yaml

A PEM needs a YAML block scalar so its newlines survive:


NAVIGATOR_GITHUB_APP_PRIVATE_KEY: |
  -----BEGIN RSA PRIVATE KEY-----
  ...
  -----END RSA PRIVATE KEY-----

Then push it to that deployment's own Secret Manager. The CSI driver projects the new version into the pods:


navigator ops secrets apply --deployment <row> --deployments-dir . --dry-run
navigator ops secrets apply --deployment <row> --deployments-dir .

The dry run prints the target project and object names without decrypting anything, so it needs no KMS permission — run it first to confirm you are aimed at the deployment you meant.

Public coordinates are not key material and do not belong in this file. A GitHub App ID, an installation ID, and an OAuth client ID are greppable, diffable, reviewable values: they live in config.toml. Only the App private key and the OAuth client secret cross into secrets.enc.yaml.

Google OAuth: six clients, not three shared secrets

Create two OAuth clients per deployment in its row's GCP project:

  • a Web application client for browser sign-in, with exactly the callback in the table;
  • a Gemini Enterprise MCP client for that deployment's data store.

That is six clients total. The staging pair lives in the neon-law-stg GCP project's Google Auth Platform consent configuration. The three production pairs live in their matching projects. Store the browser ID in NAVIGATOR_OAUTH_CLIENT_ID_BROWSER, its secret in OAUTH_CLIENT_SECRET, and the Gemini ID in NAVIGATOR_OAUTH_CLIENT_ID_GEMINI. The Gemini client secret belongs in that deployment's Gemini data-store setup. Google matches a browser redirect exactly, so do not put several sites' callbacks on one client. An Internal audience requires the project to belong to the matching Workspace organization; an External audience needs test users or the applicable verification and domain-ownership work.

The staging browser client exists with the exact name and callback in this section. Its consent configuration is External/Testing, and the authenticated operator is its initial test user. Its deployment config carries only that browser ID and secret. The Gemini ID remains absent until the data store assigns it; ops ship temporarily renders a browser-only allowlist, and #1126 removes that seam after the authenticated staging AIDA smoke test.

These clients are configured in Google Auth Platform → Clients. They are general OAuth clients, not IAP or Workforce Identity Federation clients. Google does not permit creating or modifying them programmatically, so neither ordinary gcloud services enable/service-account commands nor gcloud iam oauth-clients replace this console step.

Create a browser OAuth client and save it safely

Do this once for each row that does not already have its browser client. The Google Auth Platform page shown below is safe to include in an operating record because it contains the client name only—never capture the following creation dialog, which reveals the secret once.

Google Auth Platform browser client form

  1. In the Google Cloud console, switch to the GCP project in the deployment matrix for the target deployment. This is neon-law for neon-law-prod, neon-law-prod for neon-law-prod, and neon-law-stg for neon-law-stg. Do not create a client's credentials in a different project and copy them across.

  2. Open Google Auth Platform → Clients, then select Create client. If Google first opens the Auth Platform setup screen, complete the organization-approved branding, support contact, audience, and contact-email setup before continuing. That one-time configuration is project-scoped; it is not a replacement for the client below.

  3. Choose OAuth client ID, set application type to Web application, and name it navigator-<deployment>-browser. For example, Neon Law production is navigator-neon-law-prod-browser.

  4. Leave Authorized JavaScript origins empty. Under Authorized redirect URIs, add exactly the single callback for that config from the preceding table. Do not add a wildcard, a second deployment's callback, or a guessed local URL.

  5. Select Create. Google displays a dialog with two values: Client ID and Client secret. Copy both before closing it. The secret cannot be recovered from this dialog later; create a replacement client if it is lost.

  6. In that deployment's deployments/<name>/ tree, save the values under these exact names:

    Google creation-dialog valueWhere it lives
    Client IDNAVIGATOR_OAUTH_CLIENT_ID_BROWSER in config.toml
    Client secretOAUTH_CLIENT_SECRET in secrets.enc.yaml (sops set)

    Save them only in their matching deployment. The Client ID is public metadata but is still deployment-specific; the client secret is a credential and must never go in a terminal transcript, Slack, this repository in plaintext, or a screenshot.

  7. Reopen Google Auth Platform → Clients and verify the client name and its single redirect URI. Then run the normal secret synchronization and ship dry run. The deployment operator preflight is deliberately non-interactive: it refuses a row whose NAVIGATOR_OAUTH_CLIENT_ID_BROWSER or OAUTH_CLIENT_SECRET is absent and prints that row's project, client name, and exact callback. It verifies presence, not that a non-empty value is a usable Google credential.

Do not recycle a browser client, callback, or secret across rows. The Gemini client ID, when a deployment's data store assigns one, remains NAVIGATOR_OAUTH_CLIENT_ID_GEMINI; it is not a substitute for either browser value above.

Delete the obsolete navigator-neon-law-stg-browser, navigator-neon-law-stg-gemini, navigator-neon-staging-browser, and navigator-neon-staging-gemini registrations from the neon-law-stg project. Their retired configs are not proof that the Google registrations are gone, and no surviving deployment may reuse one of those client IDs, callbacks, or secrets.

GitHub: four organizations and four private Apps

For each organization, a GitHub owner must:

  1. confirm the existing GitHub Free organization, contact email, controlling business, and current sole owner; add no bootstrap invitation, then handle two-factor enforcement and any approved recovery owner in a separate access review;
  2. confirm the private, organization-owned GitHub App in the table is installed only in that organization;
  3. grant repository Contents and Issues read/write;
  4. put that row's organization, App ID, and private key in NAVIGATOR_GITHUB_ORG, NAVIGATOR_GITHUB_APP_ID, NAVIGATOR_GITHUB_APP_PRIVATE_KEY, and optionally pin the discovered NAVIGATOR_GITHUB_INSTALLATION_ID.

The Apps own no Project repositories: Navigator provisions none. What is left is the neon-law-foundation/navigator webhook and the DevX Restate services, which are the neon-law-stg singleton.

Provider signups that require a human account owner

  • DocuSign: create Developer/demo attachments for staging. Production needs production eSignature accounts and Go-Live-approved integrations. The deployment's secrets.enc.yaml receives the account/base/OAuth IDs, JWT app/user/private key, signer, Connect HMAC, and path secret. Prove a completed demo or deliberate production envelope and its verified Connect delivery.
  • Twilio SendGrid: create account or subuser boundaries with four separately revocable keys and webhook configurations, and authenticate each sender domain. The deployment's secrets.enc.yaml receives the mail API key, From address, inbound and event secrets, and signed-event public key. Prove outbound delivery, inbound parse, and a signed event callback.
  • Google Workspace Drive: create three Shared Drives and service accounts. A Super Admin grants Drive domain-wide delegation to each service-account OAuth client. The deployment's tree receives the selected Drive ID and delegated user (config.toml) and service-account JSON (secrets.enc.yaml). Prove a synthetic file can be created, read, and archived only in the matching Drive.
  • Restate Cloud: arrange an account and plan supporting at least three environments; the free tier is insufficient. The deployment's secrets.enc.yaml receives that row's broker, ingress, admin URL, and API key. Prove the worker registers and completes a durable workflow in that environment.
  • Production contracts: an API-capable DocuSign plan and a SendGrid plan supporting the required webhook count may be paid subscriptions. A production deployment must not contain a demo DocuSign host, demo account, or staging sender.

For SendGrid, create a restricted mail-send API key rather than a billing-capable key and enable signed webhook verification. For Drive, authorize only https://www.googleapis.com/auth/drive. For Restate, never point two rows at the same environment in the target state: its journal is state, not a stateless endpoint.

Keep provider attachments deployment-local

Do not copy a production provider bundle from staging or another brand. Each deployment's deployments/<name>/ tree owns its GitHub App, Restate journal, DocuSign attachment, SendGrid credentials and webhooks, OAuth clients, Drive root, database, session key, and application-signing keys. Run the names-only parity gate (it runs inside the workspace test suite on every pull request) and the provider smoke tests per deployment, then let a --dry-run ship enforce the actual boot-key contract.

ops ship --dry-run detects a missing or empty boot key before it applies a workload, but it cannot prove that a non-empty provider credential is valid. Treat a placeholder such as SG.test only as a bounded diagnostic that proves the next preflight branch; replace it with the deployment's restricted SendGrid key and run the outbound, inbound, and signed-event smoke tests before a production ship. A placeholder can let a pod boot while the first real email fails, which is useful evidence during setup but never production readiness.

When a dry-run prints a kubectl patch secret remedy, read it as the exact missing-key diagnosis—not as the durable repair. Add the value only to the named deployment's secrets.enc.yaml (or config.toml for a coordinate), then run navigator ops secrets apply --deployment <name> and rerun the same ops ship --dry-run. A hand-patched Kubernetes Secret is drift the CSI projection will replace. Work one reported requirement at a time: the guard stops before the workload apply, so the next dry-run is the authoritative check for the next missing boot dependency.

Before the first ship, write each deployment's Secret Manager objects from its tree:


navigator ops secrets apply --deployment <row> --deployments-dir . --dry-run
navigator ops secrets apply --deployment <row> --deployments-dir .

Repeat per deployments/ directory. The dry run reads key names only — no KMS call, no decrypted value — and fails closed listing any object the SecretProviderClass projects that the tree does not supply. It never writes DNSimple, gcloud, or operator-session values into Secret Manager; the tree cannot even express them.

Presence is only the first audit, and it runs in CI: the parity gate in cli/src/devx/deployments.rs checks every deployment's key names against store::deployment::WEB_REQUIREMENTS on every PR. Then run navigator ops ship --deployment <name> --dry-run and perform the provider smoke tests above. Never paste a private key, token, service-account JSON, or decrypted value into Slack or workshop notes — see docs/deployment-secrets.md.