Secret Management

Secret Management — Pulumi ESC + GCP Secret Manager

All application secrets — third-party API keys, webhook signatures, database connection strings, auth signing keys — live in GCP Secret Manager (the runtime store, inside the Google BAA), with Pulumi ESC (Environments, Secrets, and Configuration) as the declarative source of truth. There is no database-resident vault.

This replaces the older Supabase Vault design. With Supabase removed, a DB-resident vault is both unavailable (Neon does not support supabase_vault / pgsodium) and undesirable — DB-resident secrets would be copied into every preview branch by branch-per-PR, exactly the wrong direction.


Core Philosophy: Declarative Source of Truth → IAM-Scoped Runtime Store

  • Pulumi ESC holds the declarative definition of every environment's config + secrets, alongside the Pulumi IaC we already own. It can also mint short-lived, dynamic credentials rather than storing static ones.
  • GCP Secret Manager is the runtime store: covered by the Google BAA, IAM-scoped, and natively integrated with Cloud Run (secrets mounted as env vars or files at deploy).
Pulumi ESC (declarative — source of truth)
    → materialize → GCP Secret Manager (runtime — Google BAA, IAM-scoped)
        → mounted into Cloud Run at deploy time

How materialization works

"Materialize" is a concrete Pulumi step, not hand-copying. The Pulumi program in infra/ imports the ESC environment and, for each declared secret, creates a gcp.secretmanager.Secret + SecretVersion resource whose value comes from ESC:

// infra/ — conceptual: ESC env → Secret Manager resources
const env = esc.getEnvironment({ organization, project, environment: "prod" });
for (const [name, value] of Object.entries(env.secrets)) {
  const secret = new gcp.secretmanager.Secret(name, { secretId: name, replication: { auto: {} } });
  new gcp.secretmanager.SecretVersion(`${name}-v`, { secret: secret.id, secretData: value });
}

So ESC is the declarative source of truth, the Pulumi run materializes it into Secret Manager (the runtime store), and Cloud Run mounts the resulting Secret Manager versions at deploy. Rotating a value in ESC and re-running Pulumi adds a new SecretVersion; the old version is destroyed after cutover.

Mount secrets into Cloud Run at deploy, not fetched per request. Destroy old Secret Manager versions after a rotation cuts over.


What lives here

  • Neon per-branch DATABASE_URLs
  • Better Auth secrets / JWT signing keys
  • Vendor API keys (Telnyx, Sanity, etc.)
  • OAuth client secrets
  • Per-PR preview secrets (minted in CI, destroyed with the Neon branch)

Local Development

  • Local secrets go in a git-ignored .env file at the repo root.

  • Hydrate it from Pulumi ESC so local matches the declared shape without hand-copying:

    pulumi env open <project>/dev --format dotenv > .env   # or: esc run <env> -- <cmd>

The .env file must be listed in .gitignore. Never commit local secrets.


Production & Preview

  • Production secrets are declared in Pulumi ESC and materialized into GCP Secret Manager; Cloud Run mounts them at deploy. Rotate by adding a new Secret Manager version, then destroying the old one after cutover.
  • Preview (per-PR) secrets — including the ephemeral Neon branch DATABASE_URL and the branch-scoped Better Auth config — are minted in CI and torn down with the Neon branch on PR close (see PR Environments).

Requirements

  • Per-env isolation: prod / staging / preview-pr-* / dev.
  • Never log DATABASE_URL or any secret value. PHI-adjacent keys stay inside the boundary.
  • Rotation + audit trail (HIPAA): Secret Manager versions + GCP Cloud Audit Logs record every access.
  • Keyless CI: GitHub Actions authenticates to GCP via Workload Identity Federation — never store a GCP service-account JSON as a secret.

Quick Reference

ConcernLocal DevelopmentProduction / Preview
Source of truthPulumi ESC (pulumi env open)Pulumi ESC
Runtime store.env (git-ignored)GCP Secret Manager (mounted into Cloud Run)
Neon DATABASE_URLlocal branch / local PGSecret Manager (prod); minted per-PR, destroyed with the branch (preview)
RotationNew Secret Manager version; destroy old after cutover