Skip to main content
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. The format is specified in docs/plan-format.md.

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.
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.
nextjs-neon-preview.plan.yaml

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. 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.
nextjs-planetscale-preview.plan.yaml

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.
nextjs-supabase-preview.plan.yaml

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.
vercel-shared-and-branch-vars.plan.yaml

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:
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.
clerk-preview-callbacks.plan.yaml

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: 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:
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.
google-oauth-manual-step.plan.yaml

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.
launchdarkly-preview-flags.plan.yaml

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.
explicit-deploy.plan.yaml

production-with-approval.plan.yaml

Preview lines use a per-PR database; the production line takes its values from secrets. Applying production:
--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.
production-with-approval.plan.yaml

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.
adopting-an-existing-project.plan.yaml

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.
netlify-neon-preview.plan.yaml

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, http.list_item.
http-flags-webhooks-allowlists.plan.yaml

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.
cloudflare-pages-vars.plan.yaml

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.
cloud-run-neon-preview.plan.yaml