> ## Documentation Index
> Fetch the complete documentation index at: https://sponson.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Examples

> Complete, tested plans to start from.

Each file is a complete `release.plan.yaml`. Copy one to the root of your repository as `release.plan.yaml`, replace
the provider ids, and run `sponson plan`. Every example is checked by `scenarios/docs/examples.test.ts`: it must parse
and plan cleanly against the local fake cloud in every environment it declares, and validate against
[the schema](https://github.com/MishaBear94/Sponson/blob/main/schema/release.plan.schema.json).

The format is specified in [docs/plan-format.md](/reference/plan-format).

| File | Shows |
| - | - |
| [nextjs-neon-preview.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/nextjs-neon-preview.plan.yaml) | a database branch per pull request, wired into preview variables |
| [nextjs-planetscale-preview.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/nextjs-planetscale-preview.plan.yaml) | a PlanetScale branch and password per pull request; a once-only output |
| [nextjs-supabase-preview.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/nextjs-supabase-preview.plan.yaml) | a Supabase preview branch per pull request; its URL and connection string in preview variables; an Auth redirect on the branch |
| [vercel-shared-and-branch-vars.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/vercel-shared-and-branch-vars.plan.yaml) | project-wide, per-branch and production variables; `${ctx.*}` |
| [clerk-preview-callbacks.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/clerk-preview-callbacks.plan.yaml) | a value that exists only after the deploy; `partial` runs |
| [launchdarkly-preview-flags.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/launchdarkly-preview-flags.plan.yaml) | flags on for a preview: its URL or the scope as the target; a variation by name |
| [explicit-deploy.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/explicit-deploy.plan.yaml) | Sponson starting the deploy; `depends_on` |
| [production-with-approval.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/production-with-approval.plan.yaml) | preview and production in one file; approval |
| [adopting-an-existing-project.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/adopting-an-existing-project.plan.yaml) | what `sponson init` writes when it adopts; `{ keep: true }` |
| [netlify-neon-preview.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/netlify-neon-preview.plan.yaml) | Netlify values per deploy context; the Deploy Preview URL |
| [http-flags-webhooks-allowlists.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/http-flags-webhooks-allowlists.plan.yaml) | APIs with no adapter of their own, through the generic `http` adapter (illustrative) |
| [cloudflare-pages-vars.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/cloudflare-pages-vars.plan.yaml) | Cloudflare Pages preview and production variables; shared preview keys; write-only secrets |
| [google-oauth-manual-step.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/google-oauth-manual-step.plan.yaml) | a step no API can do (a Google OAuth redirect URI for the preview URL): a manual step, `--confirm` |
| [cloud-run-neon-preview.plan.yaml](https://github.com/MishaBear94/Sponson/blob/main/examples/cloud-run-neon-preview.plan.yaml) | a Cloud Run revision per pull request with its own URL and no traffic, using a Neon branch |

## nextjs-neon-preview\.plan.yaml

The smallest useful plan. `db` creates a Neon branch named `sponson/preview/<scope>` from the project's default branch (`production` in a new Neon project); `env` writes
`DATABASE_URL` (from the branch's sensitive `connection_string`), a host, a secret and a literal into the preview
target for the current git branch.

```text theme={null}
$ sponson plan --pr 42
sponson plan · preview · pr-42 · plan 6fb910c2

+ db   neon.branch  create   Neon branch sponson/preview/pr-42  sponson/preview/pr-42
? env  vercel.env   pending  waiting on `db`
    + DATABASE_URL (preview, feat/checkout)  (pending ← db.connection_string)
    + DATABASE_HOST (preview, feat/checkout)  (pending ← db.host)
    + STRIPE_SECRET_KEY (preview, feat/checkout)  (secret)
    + NEXT_PUBLIC_STAGE (preview, feat/checkout)  (secret)

1 to create, 1 pending
```

Every Vercel variable is shown as `(secret)` in the diff: the adapter treats all of them as sensitive.

`env` is `pending` until `db` exists; one `apply` creates both, in that order. Closing the pull request and running
`sponson apply --destroy` deletes the variables and then the branch.

```yaml nextjs-neon-preview.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A Next.js app on Vercel with a Neon Postgres database. Every pull request gets its own
# database branch, and that pull request's preview deployments get its connection string.
version: 1
providers:
  vercel: { project: prj_acme_web }   # .vercel/project.json → projectId
  neon: { project: proj_acme_db }     # Neon console → project settings → project ID

changes:
  # One Neon branch per scope. `name` defaults to sponson/<env>/<scope>, so pull request 42
  # gets `sponson/preview/pr-42`. Without `parent`, it forks from the project's default branch
  # (`production` in a new Neon project); `parent: <name or id>` picks another.
  - id: db
    adapter: neon
    op: branch
    environments: [preview]

  # Preview variables. With target preview and no `branch:`, they apply to the current git
  # branch only, so two pull requests never see each other's database.
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      DATABASE_URL: { from: db.connection_string }             # a sensitive output: never printed, never in a receipt
      DATABASE_HOST: { from: db.host }                         # a plain output
      STRIPE_SECRET_KEY: { secret: "env://STRIPE_SECRET_KEY" } # read from the environment when Sponson runs
      NEXT_PUBLIC_STAGE: preview                               # a literal
    environments: [preview]
```

## nextjs-planetscale-preview\.plan.yaml

The same shape with PlanetScale. `db` creates a development branch `sponson-preview-<scope>` from `main` and waits
until PlanetScale reports it ready; `dbpw` creates a password on it; `env` writes the password's `connection_string`
to `DATABASE_URL`. On the first plan, `dbpw` and `env` are `pending`; one `apply` creates all three, in that order.

PlanetScale shows a password's plaintext only when it is created, so `connection_string` is a
[once-only output](/reference/plan-format#once-only-outputs). `env` receives it in the run that creates the password.
On every later run, `env` keeps the value it holds: the plan shows all three lines unchanged and `apply` writes nothing.
If `DATABASE_URL` is deleted in Vercel, `env` is refused with `OUTPUT_UNAVAILABLE`, because no later run can read the
value again. Sponson does not rotate the password on its own. To get a new one, delete the password in PlanetScale:
the next `apply` creates a new password and writes its connection string. `sponson apply --destroy` deletes the
variable, then the password, then the branch.

```yaml nextjs-planetscale-preview.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A Next.js app on Vercel with a PlanetScale (Vitess / MySQL) database. Every pull request gets its own
# development branch and a password on it, and that pull request's preview deployments get its connection string.
# Credentials: PLANETSCALE_SERVICE_TOKEN_ID and PLANETSCALE_SERVICE_TOKEN (a service token), VERCEL_TOKEN.
version: 1
providers:
  vercel: { project: prj_acme_web }                        # .vercel/project.json → projectId
  planetscale: { organization: acme, database: acme-db }   # https://app.planetscale.com/<organization>/<database>

changes:
  # One development branch per scope, `sponson-<env>-<scope>` (pull request 42: `sponson-preview-pr-42`),
  # from `main`. Apply waits until PlanetScale reports it ready.
  - id: db
    adapter: planetscale
    op: branch
    parent: main
    environments: [preview]

  # A password on that branch. PlanetScale shows its plaintext once, when it is created, so `connection_string`
  # is a once-only output: `env` below receives it in the same run, later runs keep what `env` holds and write
  # nothing, and Sponson never rotates the password on its own.
  - id: dbpw
    adapter: planetscale
    op: password
    branch: { from: db.name }
    role: admin
    environments: [preview]

  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      DATABASE_URL: { from: dbpw.connection_string }   # sensitive and once-only: never printed, never stored
      DATABASE_USERNAME: { from: dbpw.username }        # a plain output
      NEXT_PUBLIC_STAGE: preview
    environments: [preview]
```

## nextjs-supabase-preview\.plan.yaml

The Supabase version of the plan above. `db` creates a Supabase preview branch named `sponson-preview-<scope>` on the
project (branching must be enabled on it) and waits until it is `ACTIVE_HEALTHY`; `env` writes its sensitive
`connection_string` and its `api_url` into the preview target for the current git branch; `auth_callback` puts the
preview URL on the branch's own Auth redirect allow-list, because a branch is a project with its own Auth settings.
Without `project:`, an `auth_redirect` line edits the allow-list of `providers.supabase.project` instead, leaving every
entry Sponson did not add in place.

The branch's API keys are not an output yet, so the example passes the anon key as a secret.
`sponson apply --destroy` removes the redirect, the variables, then the branch.

```yaml nextjs-supabase-preview.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A Next.js app on Vercel with Supabase. Every pull request gets its own Supabase preview branch (a
# project of its own, with its own database, API URL and Auth settings); that pull request's preview
# deployments get the branch's connection string and URL, and the preview URL is allowed as an Auth
# redirect on the branch.
version: 1
providers:
  vercel: { project: prj_acme_web }                  # .vercel/project.json → projectId
  supabase: { project: acmewebprojectrefxyz }        # `supabase link` → supabase/.temp/project-ref; branching enabled

changes:
  # One preview branch per scope. `name` defaults to sponson-<env>-<scope>, so pull request 42 gets
  # `sponson-preview-pr-42`. Apply waits until the branch is up (SPONSON_SUPABASE_TIMEOUT_MS).
  - id: db
    adapter: supabase
    op: branch
    environments: [preview]

  # Preview variables for the current git branch only.
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      DATABASE_URL: { from: db.connection_string }   # sensitive: never printed, never in a receipt
      NEXT_PUBLIC_SUPABASE_URL: { from: db.api_url } # https://<branch ref>.supabase.co
      NEXT_PUBLIC_SUPABASE_ANON_KEY: { secret: "env://SUPABASE_PREVIEW_ANON_KEY" } # the branch's keys are not an output yet
    environments: [preview]

  # The preview's URL on the branch's own Auth redirect allow-list. `project` points the line at the
  # branch; without it, the line edits the allow-list of providers.supabase.project.
  - id: auth_callback
    adapter: supabase
    op: auth_redirect
    project: { from: db.project_ref }
    url: { from: env.preview_url }
    environments: [preview]
```

## vercel-shared-and-branch-vars.plan.yaml

Three levels of Vercel variables:

* `branch: "*"` writes project-wide preview variables that every preview deployment sees.
* Without `branch:`, preview variables apply to the current git branch only.
* `target: production` writes production variables; that line is filtered to `environments: [production]`.

Project-wide variables are owned by the scope that applied them first and are destroyed with it. Apply this plan from
the default branch (scope `main`) before pull requests use it; pull request scopes then rely on the shared variables
without owning them, and a pull request that tries to change one is refused with `OWNED_BY_OTHER_SCOPE`.

```yaml vercel-shared-and-branch-vars.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Vercel variables at three levels: project-wide preview variables, variables for one
# preview branch, and production variables. No database; Vercel only.
version: 1
environments: [preview, production]
providers:
  vercel: { project: prj_acme_web, team: team_acme }   # `team` only for projects owned by a team

changes:
  # Project-wide preview variables (`branch: "*"`): every preview deployment sees them,
  # whatever its git branch. They belong to the scope that first applies them; apply this
  # plan from the default branch first so that scope is `main`, which is never destroyed.
  - id: preview-shared
    adapter: vercel
    op: env
    target: preview
    branch: "*"
    values:
      NEXT_PUBLIC_API_BASE: https://api.staging.acme.dev
      SENTRY_AUTH_TOKEN: { secret: "env://SENTRY_AUTH_TOKEN" }
    environments: [preview]

  # Variables for the current git branch only (the default for target preview).
  # `${ctx.*}` is interpolated into strings before the adapter sees them.
  - id: preview-branch
    adapter: vercel
    op: env
    target: preview
    values:
      NEXT_PUBLIC_SCOPE: "${ctx.scope}"           # pr-42, branch-feat-x or main
      NEXT_PUBLIC_COMMIT: "${ctx.git.short_sha}"
    environments: [preview]

  # Production. Applying it needs `--env production --approved-by <who>`.
  - id: production
    adapter: vercel
    op: env
    target: production
    values:
      NEXT_PUBLIC_API_BASE: https://api.acme.dev
      SENTRY_AUTH_TOKEN: { secret: "env://SENTRY_AUTH_TOKEN" }
    environments: [production]
```

## clerk-preview-callbacks.plan.yaml

`callback` reads `env.preview_url`, which only exists once Vercel's deployment of this commit is `READY`. The first
`apply` creates the branch and the variables, then stops:

```text theme={null}
$ sponson apply
...
? callback  clerk.redirect_allow  waiting    waiting on deploy

apply partial: callback waits for the deploy
```

In `--json`, `receipt.status` is `partial` and `receipt.lines.callback` has `status: "waiting"` and
`waitingFor: "deploy"`.

Exit code 0. Run `sponson apply` again after the deploy (the README's workflow does it on `deployment_status`) and the
callback is registered. With `--wait`, one `apply` polls until the deploy finishes instead. If the deploy fails, the
callback is `skipped` with `EXTERNAL_FAILED` and the branch and variables stay.

```yaml clerk-preview-callbacks.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Each preview deployment's URL is allowed as a Clerk redirect URL. The URL exists only
# after Vercel has deployed, so the first `apply` stops with status `partial` and the
# callback line `waiting` for the deploy. The next `apply` (for example from a
# `deployment_status` workflow) finishes it. Same command, no flags.
version: 1
providers:
  vercel: { project: prj_acme_web }
  neon: { project: proj_acme_db }

changes:
  - id: db
    adapter: neon
    op: branch
    environments: [preview]

  # vercel.env declares `preview_url` and `deployment_id` as outputs that become available
  # only after the deploy of this commit is READY.
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      DATABASE_URL: { from: db.connection_string }
      CLERK_SECRET_KEY: { secret: "env://CLERK_PREVIEW_SECRET_KEY" }
    environments: [preview]

  # Reads an external output, so it waits for the deploy. A `from:` reference is the
  # whole value; it cannot be combined with other text.
  - id: callback
    adapter: clerk
    op: redirect_allow
    url: { from: env.preview_url }
    environments: [preview]
```

## google-oauth-manual-step.plan.yaml

Google has no API for a web OAuth client's redirect URIs, so `google-redirect` is a [manual step](/reference/plan-format#manual-step):
the plan says what a person must do, with the preview URL filled in. It waits for the deploy like any `from:`
reference; then `plan` shows it as `todo` with its instructions, and `apply` writes everything else, leaves the step
and `google-login` (which `depends_on` it) waiting, and exits 2:

```text theme={null}
$ sponson apply
...
? google-redirect  manual.step  waiting  waiting on confirmation
? google-login     vercel.env   waiting  waiting on confirmation

manual steps (a person does these; agents never confirm them)
  google-redirect: Allow pr-42's OAuth callback on the Google OAuth client acme-web
    | Google offers no API for a web client's redirect URIs, so this is done by hand.
    ...
    Then confirm it: sponson apply --confirm google-redirect

apply partial: google-redirect, google-login wait for the confirmation
```

After adding the URI, the person runs `sponson apply --confirm google-redirect` (as the git user, or with
`--approved-by <who>`): the receipt records who confirmed it and when, and `google-login` is applied. A later
deployment with a new preview URL makes the step a todo again. `apply --destroy` removes the variables and shows the
undo, waiting for `apply --destroy --confirm google-redirect`. In `--json`, the steps are in `manual` and the error is
`MANUAL_STEP_PENDING`; an agent shows them to a person and never confirms on its own.

```yaml google-oauth-manual-step.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Google has no API for a web OAuth client's authorized redirect URIs, so a person adds each
# preview's callback in the Google Cloud console. The plan says exactly what to do, with the
# preview URL filled in; `apply` waits for the step (exit 2, MANUAL_STEP_PENDING) and holds back
# the line that turns Google sign-in on, until a person runs `sponson apply --confirm google-redirect`.
# `apply --destroy` shows the undo and waits for the same confirmation (ADR 0021).
version: 1
providers:
  vercel: { project: prj_acme_web }

changes:
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      AUTH_GOOGLE_ID: acme-web.apps.googleusercontent.com
      AUTH_GOOGLE_SECRET: { secret: "env://GOOGLE_OAUTH_CLIENT_SECRET" }
    environments: [preview]

  # The preview URL exists after the deploy, so this step waits for it like any `from:` reference.
  - id: google-redirect
    adapter: manual
    op: step
    title: "Allow ${ctx.scope}'s OAuth callback on the Google OAuth client acme-web"
    vars: { url: { from: env.preview_url } }
    instructions: |
      Google offers no API for a web client's redirect URIs, so this is done by hand.

      1. Open https://console.cloud.google.com/apis/credentials (project `acme-web`).
      2. Open the OAuth 2.0 client **acme-web**.
      3. Under **Authorized redirect URIs**, add `{url}/api/auth/callback/google`.
      4. Save. Google may take a few minutes to accept the new URI.
    undo: |
      In the same client, remove `{url}/api/auth/callback/google` from **Authorized redirect URIs**, and save.
    environments: [preview]

  # Sign-in with Google is switched on for the preview only once the redirect URI is in place.
  - id: google-login
    adapter: vercel
    op: env
    target: preview
    depends_on: [google-redirect]
    values:
      NEXT_PUBLIC_GOOGLE_LOGIN: "on"
    environments: [preview]
```

## launchdarkly-preview-flags.plan.yaml

Two individual targets on flags that already exist in the LaunchDarkly project `acme-web`, environment `preview`.
`checkout` serves `new-checkout` = `true` to the context of kind `url` whose key is the preview URL, so like the Clerk
callback above it waits for the deploy; `pricing` serves the variation named `Treatment` of `pricing-page` to the user
key `pr-42` (the scope) and is applied at once. `apply --destroy` removes exactly these two targets; the flags, their
rules and everyone else's targets stay. A target moved to another variation in the LaunchDarkly console is `changed`
drift, refused until `--reconcile`.

```yaml launchdarkly-preview-flags.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Feature flags turned on for each pull request's preview, and off again when it closes
# (`sponson apply --destroy`). Sponson adds individual targets to flags that already exist
# in LaunchDarkly; it never creates, deletes, or switches flags. LAUNCHDARKLY_ACCESS_TOKEN
# is read from the environment.
version: 1
providers:
  vercel: { project: prj_acme_web }
  # A project key and an environment key. Use a preview or test environment: one that
  # requires approvals for flag changes refuses Sponson's writes.
  launchdarkly: { project: acme-web, environment: preview }

changes:
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      LAUNCHDARKLY_CLIENT_SIDE_ID: { secret: "env://LAUNCHDARKLY_PREVIEW_CLIENT_SIDE_ID" }
    environments: [preview]

  # The new checkout, served `true` to the preview deployment. The app evaluates flags with a
  # context of kind `url` whose key is its own URL, so the target is the preview URL: it exists
  # once Vercel has deployed this commit, and the next `apply` finishes the line.
  - id: checkout
    adapter: launchdarkly
    op: flag_target
    flag: new-checkout
    context_kind: url
    key: { from: env.preview_url }
    variation: true
    environments: [preview]

  # A multivariate flag, by variation name. `key` defaults to the scope (`pr-42`) and
  # `context_kind` to `user`: the app sends the pull request as a user key in previews.
  - id: pricing
    adapter: launchdarkly
    op: flag_target
    flag: pricing-page
    variation: Treatment
    environments: [preview]
```

## explicit-deploy.plan.yaml

A `vercel.deploy` line starts the deployment after the variables are written (`depends_on: [env]`), and its
`preview_url` is available as soon as the line is applied, so one `apply` runs to the end and waits for the build (up
to `SPONSON_DEPLOY_TIMEOUT_MS`, default 120 seconds). Use it when automatic preview deployments are turned off in
Vercel; with them on, prefer the previous example.

```yaml explicit-deploy.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Sponson starts the deployment itself, after the variables are written, so nothing waits on
# an external event and one `apply` runs to the end. Use this when automatic preview
# deployments are turned off in Vercel; otherwise prefer clerk-preview-callbacks.plan.yaml.
version: 1
providers:
  vercel: { project: prj_acme_web }

changes:
  - id: env
    adapter: vercel
    op: env
    target: preview
    values:
      FEATURE_FLAGS: "checkout-v2,search-v3"
    environments: [preview]

  # No data flows from `env` to `deploy`, but the deploy must come after the variables:
  # `depends_on` orders lines without a reference. The deploy's `preview_url` is an
  # immediate output: `apply` waits for the build (SPONSON_DEPLOY_TIMEOUT_MS, default 120 s).
  - id: deploy
    adapter: vercel
    op: deploy
    depends_on: [env]
    environments: [preview]

  - id: callback
    adapter: clerk
    op: redirect_allow
    url: { from: deploy.preview_url }
    environments: [preview]
```

## production-with-approval.plan.yaml

Preview lines use a per-PR database; the production line takes its values from secrets. Applying production:

```bash theme={null}
sponson plan  --env production              # read-only; reports requiresApproval: true
sponson apply --env production              # refused: ENV_NOT_APPROVED (exit 2), nothing written
sponson apply --env production --approved-by alice
```

`--approved-by` (or `SPONSON_APPROVED_BY`) should come from a person or an approval workflow, never from an agent.
The approver is recorded in the receipt as `approvedBy`.

```yaml production-with-approval.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Preview and production in one file. Lines are filtered by `environments:`; `--env` picks
# the environment (default preview; production is never inferred from a branch name).
# Any run that writes to production is refused with ENV_NOT_APPROVED unless an approver
# is named: `sponson apply --env production --approved-by alice`.
version: 1
environments: [preview, production]
providers:
  vercel: { project: prj_acme_web }
  neon: { project: proj_acme_db }

changes:
  - id: db
    adapter: neon
    op: branch
    environments: [preview]

  - id: env-preview
    adapter: vercel
    op: env
    target: preview
    values:
      DATABASE_URL: { from: db.connection_string }
      STRIPE_SECRET_KEY: { secret: "env://STRIPE_TEST_SECRET_KEY" }
    environments: [preview]

  # Production values come from secrets, never from another line: there is no per-PR
  # database in production. Without `environments: [production]`, this line would also run
  # in preview, and every preview run would then need approval because it writes to production.
  - id: env-production
    adapter: vercel
    op: env
    target: production
    values:
      DATABASE_URL: { secret: "env://PRODUCTION_DATABASE_URL" }
      STRIPE_SECRET_KEY: { secret: "env://STRIPE_LIVE_SECRET_KEY" }
    environments: [production]
```

## adopting-an-existing-project.plan.yaml

What `sponson init` appends to an existing plan when it finds resources no scope manages: variables become
`{ keep: true }`, grouped by target and git branch; an existing Neon branch is adopted by its `name:`. Nothing live
changes, and adopted resources are never destroyed. Run `sponson init --adopt <key>` to adopt a single resource; the
keys are listed under `drift` (`kind: unmanaged`) in `sponson plan --json`.

```yaml adopting-an-existing-project.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A project that existed before Sponson. These lines are what `sponson init` appends when it
# adopts unmanaged resources: values are never copied into the plan, only `{ keep: true }`
# ("whatever is live"). Adopted resources are never destroyed by `apply --destroy`.
version: 1
environments: [preview, production]
providers:
  vercel: { project: prj_acme_web }
  neon: { project: proj_acme_db }

changes:
  # Project-wide preview variables someone set in the dashboard. Sponson now tracks that they
  # exist and reports drift; it does not manage their values. A `_KEY` name may be `{ keep: true }`:
  # only literal strings and numbers are refused for secret-looking names.
  - id: env-preview-shared
    adapter: vercel
    op: env
    target: preview
    branch: "*"
    values:
      SENTRY_DSN: { keep: true }
      NEXT_PUBLIC_POSTHOG_KEY: { keep: true }
    environments: [preview]

  - id: env-production
    adapter: vercel
    op: env
    target: production
    values:
      DATABASE_URL: { keep: true }
    environments: [production]

  # An existing Neon branch, identified by its name.
  - id: db-staging
    adapter: neon
    op: branch
    name: staging
    environments: [preview]

  # To let Sponson manage a value, replace `{ keep: true }` with a literal or a reference,
  # for example `SENTRY_DSN: { secret: "env://SENTRY_DSN" }`.
```

## netlify-neon-preview\.plan.yaml

The Neon branch per pull request on a Netlify site. Netlify stores one variable per name with one value per deploy
context; a `netlify.env` line owns one context's value of each variable it declares, so values a human set for other
contexts are never read, changed or deleted. In previews the line writes the value for the pull request's git branch
(`context: branch`), which Netlify uses for that branch's Deploy Previews; `context: deploy-preview` would instead
be one value shared by every pull request.

`callback` reads `env.deploy_preview_url`, the pull request's `https://deploy-preview-<n>--<site>.netlify.app`. Like
Vercel's `preview_url`, it exists only once the Deploy Preview of this commit is ready, and only a deploy that
started after the variables were written counts: a build reads the values set when it started.

```yaml netlify-neon-preview.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A Netlify site with a Neon branch per pull request. Netlify keeps one value per deploy
# context; in previews this plan writes the value for the pull request's git branch, which
# Netlify uses for that branch's Deploy Previews, so two pull requests never share one.
version: 1
providers:
  # The site's Project ID (Project configuration → General, or `siteId` in .netlify/state.json).
  # `account` (the team slug) is optional: without it the site's own account is used.
  netlify: { site: 3f1c2a9e-0b7d-4e55-9c1a-acme00000001 }
  neon: { project: proj_acme_db }

changes:
  - id: db
    adapter: neon
    op: branch
    environments: [preview]

  # context defaults to `branch` (this git branch) in previews and `production` under --env production.
  - id: env
    adapter: netlify
    op: env
    values:
      DATABASE_URL: { from: db.connection_string }
    environments: [preview]

  # Production values next to the ones a human set in the Netlify UI: Sponson owns only
  # the production value of the variables it declares. Requires approval.
  - id: env-production
    adapter: netlify
    op: env
    context: production
    values:
      FEATURE_CHECKOUT_V2: "off"
    environments: [production]

  # deploy_preview_url (https://deploy-preview-<n>--<site>.netlify.app) exists once the Deploy
  # Preview of this commit, built after the variables were written, is ready.
  - id: callback
    adapter: clerk
    op: redirect_allow
    url: { from: env.deploy_preview_url }
    environments: [preview]
```

## http-flags-webhooks-allowlists.plan.yaml

**Illustrative.** The generic `http` adapter managing what has no Sponson adapter: a feature gate per pull request
(`http.resource`, located by name), a test-mode webhook endpoint (form-encoded, with an idempotency key and a
sensitive output), the preview URL on an application's allowed web origins (`http.list_item`, an item of a JSON
array), and a callback on a comma-separated redirect allow-list (`shape: delimited`). Each API is a block under
`providers.http` naming its credential's environment variable. The endpoints follow the providers' published API
references as we understand them; plan against the real API (`sponson plan` only reads) before the first apply.
`apply --destroy` removes the gate, the endpoint and the two list items, and leaves every other origin and
redirect as it was. Reference: [`http.resource`](/reference/plan-format#http-resource),
[`http.list_item`](/reference/plan-format#http-list_item).

```yaml http-flags-webhooks-allowlists.plan.yaml theme={null}
# yaml-language-server: $schema=../schema/release.plan.schema.json
#
# ILLUSTRATIVE. The generic `http` adapter driving four SaaS APIs that have no Sponson adapter of their own. The
# endpoints, envelopes and credential headers follow each provider's published API reference as we understand it;
# check them against the current docs before relying on this plan, and run `sponson plan` first: it only reads.
# The ids (Auth0 client, Supabase project ref, tenant domain) are placeholders.
#
# Each `base_url_env` is an optional override of `base_url` (a staging API; the test suite points them at the
# local fake cloud). Credentials are environment variable NAMES; Sponson never stores or prints their values.
version: 1
environments: [preview]
providers:
  vercel: { project: prj_acme_web }
  http:
    statsig:
      base_url: https://statsigapi.net/console/v1
      base_url_env: STATSIG_API_URL
      auth: { header: STATSIG-API-KEY, value_env: STATSIG_CONSOLE_KEY }
    stripe:
      base_url: https://api.stripe.com/v1
      base_url_env: STRIPE_API_URL
      auth: { bearer_env: STRIPE_TEST_SECRET_KEY }
      encoding: form
    auth0:
      base_url: https://acme.us.auth0.com/api/v2
      base_url_env: AUTH0_API_URL
      auth: { bearer_env: AUTH0_MANAGEMENT_TOKEN }
    supabase:
      base_url: https://api.supabase.com/v1
      base_url_env: SUPABASE_API_URL
      auth: { bearer_env: SUPABASE_ACCESS_TOKEN }
changes:
  # The preview deployment; its URL exists once Vercel's build of this commit is ready.
  - id: web
    adapter: vercel
    op: env
    target: preview
    values:
      NEXT_PUBLIC_CHECKOUT_GATE: "checkout_${ctx.scope}"

  # A Statsig feature gate per pull request, on for the preview. Located by name in GET /gates.
  - id: gate
    adapter: http
    op: resource
    api: statsig
    find: { path: /gates, list_path: /data, match: { name: "checkout_${ctx.scope}" } }
    read: { path: "/gates/{id}" }
    create: { path: /gates }
    update: { method: PATCH }
    item_path: /data
    fields:
      description: "New checkout, preview of ${ctx.scope}"
      isEnabled: true

  # A Stripe test-mode webhook endpoint for the preview. Stripe takes form-encoded bodies, updates with POST, and
  # honours Idempotency-Key. Its signing secret is in the create answer only: a sensitive output, available to the
  # run that creates the endpoint (see ADR 0017 for why a later run cannot read it again).
  - id: stripe_hook
    adapter: http
    op: resource
    api: stripe
    find: { path: "/webhook_endpoints?limit=100", list_path: /data, match: { url: "https://${ctx.scope}.preview.acme.dev/api/webhooks/stripe" } }
    read: { path: "/webhook_endpoints/{id}" }
    create: { path: /webhook_endpoints, idempotency_key: true }
    update: { method: POST }
    fields:
      enabled_events: [checkout.session.completed, invoice.paid]
      description: "Preview ${ctx.scope}"
    outputs:
      signing_secret: { path: /secret, sensitive: true }

  # The preview URL on an Auth0 application's allowed web origins: one item of a list on the client object.
  - id: auth0_origin
    adapter: http
    op: list_item
    api: auth0
    vars: { client: aBcD1234eFgH5678 }
    parent: { path: "/clients/{client}", read_path: "/clients/{client}?fields=web_origins" }
    list_path: /web_origins
    item: { from: web.preview_url }

  # The preview's auth callback on Supabase's redirect allow-list, which is one comma-separated string.
  - id: supabase_redirect
    adapter: http
    op: list_item
    api: supabase
    parent: { path: /projects/abcdefghijklmnopqrst/config/auth }
    list_path: /uri_allow_list
    shape: delimited
    item: "https://${ctx.scope}.preview.acme.dev/**"
```

## cloudflare-pages-vars.plan.yaml

A Cloudflare Pages project's variables: plain text under `vars:`, secrets under `secrets:`. Pages has one set of
preview variables for every preview deployment, so `pages-preview` holds only values that are the same for every pull
request; apply it from the default branch first (scope `main`), and pull requests then rely on it without owning it.
A pull request that sets another value for one of these keys is refused with `OWNED_BY_OTHER_SCOPE`. Each line's
`target` must equal the run's `--env`; the production line requires approval like any other.

Cloudflare never returns a secret's value, so Sponson compares secrets by presence and type: `pages-production` sets
`rewrite_secrets: true` so that a rotated `STRIPE_LIVE_SECRET_KEY` is written on the next apply. Variables reach the
next deployment; Sponson does not redeploy a Pages project.

```yaml cloudflare-pages-vars.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# Cloudflare Pages variables for preview and production. Pages has ONE set of preview variables,
# shared by every preview deployment of the project (there are no per-branch values), so the
# preview line holds only values that are the same for every pull request: the first scope that
# writes a key owns it, and a second pull request that sets another value is refused with
# OWNED_BY_OTHER_SCOPE. Apply it from the default branch first; pull requests then rely on it.
#
# Credentials: CLOUDFLARE_API_TOKEN (account permission "Cloudflare Pages Edit").
version: 1
environments: [preview, production]
providers:
  cloudflare: { account: 0123456789abcdef0123456789abcdef, project: acme-web }

changes:
  # `target` defaults to the run's environment and must equal it.
  - id: pages-preview
    adapter: cloudflare
    op: pages_env
    vars:
      PUBLIC_API_URL: https://api.staging.acme.dev
      PUBLIC_STAGE: preview
    secrets:
      STRIPE_SECRET_KEY: { secret: "env://STRIPE_TEST_SECRET_KEY" }
    environments: [preview]

  # Secrets are compared by presence and type only: Cloudflare never returns their values. A
  # rotated secret is written when the line sets `rewrite_secrets: true` (every apply sends them).
  - id: pages-production
    adapter: cloudflare
    op: pages_env
    target: production
    vars:
      PUBLIC_API_URL: https://api.acme.dev
      PUBLIC_STAGE: production
    secrets:
      STRIPE_SECRET_KEY: { secret: "env://STRIPE_LIVE_SECRET_KEY" }
    rewrite_secrets: true
    environments: [production]
```

## cloud-run-neon-preview\.plan.yaml

A Neon branch per pull request and a revision of the existing Cloud Run service `web` that uses it: the image CI
pushed for this commit, the branch's connection string, a secret and a literal, made from the service's template and
tagged with the scope (`pr-42`) with no share of traffic. The line's `url` output is the tag's own URL, read from
Cloud Run (here allowed as a Clerk callback). Production's traffic stays where it is: the example assumes it is on a
named revision. A service whose traffic follows LATEST is refused until the line sets `pin_latest: true`, which pins
that share to the revision serving it, as `gcloud run deploy --no-traffic` does. `apply --destroy` removes the
tag and the Neon branch; the untagged revision is left for Cloud Run, which deletes old ones past its per-service
limit. Reference:
[`cloud_run.revision`](/reference/plan-format#cloud_run-revision).

```yaml cloud-run-neon-preview.plan.yaml theme={null}
# yaml-language-server: $schema=https://raw.githubusercontent.com/MishaBear94/Sponson/main/schema/release.plan.schema.json
#
# A Neon branch per pull request, and a revision of the Cloud Run service `web` per pull request that uses it:
# the image CI built for this commit, the branch's connection string, tagged `pr-42` (the scope) and serving no
# traffic. Its own URL is the `url` output. Needs gcloud on PATH, logged in (in GitHub Actions:
# google-github-actions/auth, then setup-gcloud), and NEON_API_KEY.
version: 1
environments: [preview, production]
providers:
  neon: { project: proj_acme_web }
  cloud_run: { project: acme-web-prod, region: europe-west1 }

changes:
  - id: db
    adapter: neon
    op: branch
    environments: [preview]

  # Production's traffic stays where it is. This assumes it is on a named revision
  # (`gcloud run services update-traffic web --to-revisions=<revision>=100`). If it follows LATEST, the line is
  # refused until you pin it yourself or add `pin_latest: true`, which pins LATEST's share to the revision serving it;
  # see docs/plan-format.md#cloud_runrevision for what that means for later production deploys.
  - id: preview
    adapter: cloud_run
    op: revision
    service: web
    image: "europe-west1-docker.pkg.dev/acme-web-prod/web/app:${ctx.git.sha}"
    env:
      DATABASE_URL: { from: db.connection_string }
      SESSION_SECRET: { secret: "env://PREVIEW_SESSION_SECRET" }
      APP_ENV: preview
    environments: [preview]

  # The preview's own URL, for anything that needs it: here, a callback allowed on the Clerk instance.
  - id: callback
    adapter: clerk
    op: redirect_allow
    url: { from: preview.url }
    environments: [preview]
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.