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.
(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: productionwrites production variables; that line is filtered toenvironments: [production].
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:
--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, sogoogle-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:
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 projectacme-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
Avercel.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
Whatsponson 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; anetlify.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 generichttp 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 undervars:, 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 serviceweb 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