> ## 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.

# Supported providers

> Every provider Sponson manages, by category: what it manages, the op or recipe, the credential and a line to start from.

Every provider whose per-environment state Sponson manages, by category: what it manages, the op or recipe that
does it, the credential it reads from the environment (where to create it, and what to pick: `sponson init --setup` walks
through the same), what `sponson init` detects it by, and a line to start from.
`sponson init` reads dependency names, config files and variable names (never their values) and writes these lines
for every provider it detects: ready to apply when it knows every value, else commented out with a `# TODO:` that
says where to find each missing one.

64 providers in 13 categories: 17 ops of first-class adapters and 68 recipe ops, plus 3 steps no API reaches, tracked as [`manual.step`](/reference/plan-format#manual-step) lines. `sponson init` detects 59 providers.

* First-class ops: [the plan format](/reference/plan-format#built-in-ops). Recipes: [Recipes](/reference/recipes).
* What is covered and what is not, and why: [Provider coverage](/reference/coverage).
* A provider that is missing: add a recipe (one YAML file; its `detect` block makes `sponson init` find it), see
  [CONTRIBUTING.md](/contributing/contributing).

| Category | Providers |
| - | - |
| [Deploy targets and their environment variables](/reference/providers#deploy-targets-and-their-environment-variables) | [Vercel](/reference/providers#vercel), [Netlify](/reference/providers#netlify), [Cloudflare Pages / Workers](/reference/providers#cloudflare-pages-/-workers), [Render](/reference/providers#render), [Railway](/reference/providers#railway), [Fly.io](/reference/providers#fly-io), [Heroku](/reference/providers#heroku), [AWS Amplify](/reference/providers#aws-amplify), [Google Cloud Run](/reference/providers#google-cloud-run), [Azure Static Web Apps](/reference/providers#azure-static-web-apps), [DigitalOcean App Platform](/reference/providers#digitalocean-app-platform), [Firebase Hosting](/reference/providers#firebase-hosting) |
| [Databases with branching or per-PR instances](/reference/providers#databases-with-branching-or-per-pr-instances) | [Neon](/reference/providers#neon), [Supabase](/reference/providers#supabase), [PlanetScale](/reference/providers#planetscale), [Turso](/reference/providers#turso), [Xata](/reference/providers#xata), [MongoDB Atlas](/reference/providers#mongodb-atlas), [CockroachDB Cloud](/reference/providers#cockroachdb-cloud), [Prisma Postgres](/reference/providers#prisma-postgres) |
| [Auth callback and redirect allow-lists](/reference/providers#auth-callback-and-redirect-allow-lists) | [Clerk](/reference/providers#clerk), [Auth0](/reference/providers#auth0), [Supabase Auth](/reference/providers#supabase-auth), [Firebase Auth](/reference/providers#firebase-auth), [WorkOS](/reference/providers#workos), [Stytch](/reference/providers#stytch), [Okta](/reference/providers#okta), [Kinde](/reference/providers#kinde), [Google OAuth clients](/reference/providers#google-oauth-clients) |
| [Feature flags and targeting](/reference/providers#feature-flags-and-targeting) | [LaunchDarkly](/reference/providers#launchdarkly), [PostHog](/reference/providers#posthog), [Statsig](/reference/providers#statsig), [GrowthBook](/reference/providers#growthbook), [Unleash](/reference/providers#unleash), [Flagsmith](/reference/providers#flagsmith), [ConfigCat](/reference/providers#configcat), [Split (Harness FME)](/reference/providers#split-harness-fme) |
| [Webhooks registered per environment](/reference/providers#webhooks-registered-per-environment) | [Stripe](/reference/providers#stripe), [GitHub](/reference/providers#github), [Svix (as a sender)](/reference/providers#svix-as-a-sender), [Clerk webhooks](/reference/providers#clerk-webhooks), [Twilio](/reference/providers#twilio), [Ably](/reference/providers#ably) |
| [Email: sending domains, webhooks, test inboxes](/reference/providers#email-sending-domains-webhooks-test-inboxes) | [Resend](/reference/providers#resend), [Postmark](/reference/providers#postmark), [SendGrid](/reference/providers#sendgrid), [Mailgun](/reference/providers#mailgun) |
| [Observability per release](/reference/providers#observability-per-release) | [Sentry](/reference/providers#sentry), [Datadog](/reference/providers#datadog), [Honeycomb](/reference/providers#honeycomb), [New Relic](/reference/providers#new-relic), [Rollbar](/reference/providers#rollbar), [BugSnag](/reference/providers#bugsnag) |
| [DNS and preview domains](/reference/providers#dns-and-preview-domains) | [Cloudflare DNS](/reference/providers#cloudflare-dns), [Vercel domains](/reference/providers#vercel-domains) |
| [CORS and allowed origins](/reference/providers#cors-and-allowed-origins) | [Firebase Storage (GCS bucket)](/reference/providers#firebase-storage-gcs-bucket), [S3 / R2](/reference/providers#s3-/-r2), [Sanity CORS](/reference/providers#sanity-cors) |
| [Queues, caches, background jobs](/reference/providers#queues-caches-background-jobs) | [Upstash Redis](/reference/providers#upstash-redis), [Upstash QStash](/reference/providers#upstash-qstash), [Inngest](/reference/providers#inngest) |
| [Search, payments test mode](/reference/providers#search-payments-test-mode) | [Algolia](/reference/providers#algolia), [Stripe test-mode objects](/reference/providers#stripe-test-mode-objects), [Stripe sandboxes](/reference/providers#stripe-sandboxes) |
| [Headless CMS content per preview](/reference/providers#headless-cms-content-per-preview) | [Contentful](/reference/providers#contentful), [Sanity](/reference/providers#sanity) |
| [Backend platforms with preview deployments](/reference/providers#backend-platforms-with-preview-deployments) | [Convex](/reference/providers#convex) |

## Deploy targets and their environment variables

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Vercel](/reference/providers#vercel) | Env vars per target and git branch; wait for the preview deployment, redeploy if it built before the write | [`vercel.env`](/reference/plan-format#vercel-env), [`vercel.deploy`](/reference/plan-format#vercel-deploy) | `VERCEL_TOKEN` |
| [Netlify](/reference/providers#netlify) | Env var values per deploy context (`deploy-preview`, `branch` + `context_parameter`); wait for the deploy of the commit, rebuild if it started before the write | [`netlify.env`](/reference/plan-format#netlify-env) | `NETLIFY_AUTH_TOKEN` |
| [Cloudflare Pages / Workers](/reference/providers#cloudflare-pages-/-workers) | Pages: `deployment_configs.preview.env_vars` (all previews share one set, no per-branch values). Workers: secrets per script / preview version | [`cloudflare.pages_env`](/reference/plan-format#cloudflare-pages_env) | `CLOUDFLARE_API_TOKEN` |
| [Render](/reference/providers#render) | Env vars on a preview service / preview environment | recipe [`render.env_var`](/reference/recipes#render-env_var) | `RENDER_API_KEY` (bearer) |
| [Railway](/reference/providers#railway) | Variables in a PR environment (Railway creates PR environments natively) | recipe [`railway.variable`](/reference/recipes#railway-variable) | `RAILWAY_API_TOKEN` (bearer) |
| [Fly.io](/reference/providers#fly-io) | Per-PR app (the usual pattern is one app per PR) plus its secrets | recipe [`fly_machines.app`](/reference/recipes#fly_machines-app), recipe [`fly.secret`](/reference/recipes#fly-secret) | `FLY_API_TOKEN` (bearer) |
| [Heroku](/reference/providers#heroku) | Review app per PR; config vars on it | recipe [`heroku.config_var`](/reference/recipes#heroku-config_var) | `HEROKU_API_KEY` (bearer) |
| [AWS Amplify](/reference/providers#aws-amplify) | Branch (per-PR previews built in); env vars per branch | recipe [`amplify.branch`](/reference/recipes#amplify-branch), recipe [`amplify.branch_variable`](/reference/recipes#amplify-branch_variable) | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (AWS SigV4, `amplify` in `us-east-1`) |
| [Google Cloud Run](/reference/providers#google-cloud-run) | Tagged revision per PR (`--tag pr-42`), env vars on it | [`cloud_run.revision`](/reference/plan-format#cloud_run-revision) | the token `gcloud auth print-access-token` prints (bearer) |
| [Azure Static Web Apps](/reference/providers#azure-static-web-apps) | Preview environment per PR (built in); app settings | recipe [`azure_swa.build_app_setting`](/reference/recipes#azure_swa-build_app_setting) | the token `az account get-access-token --query accessToken -o tsv` prints (bearer) |
| [DigitalOcean App Platform](/reference/providers#digitalocean-app-platform) | No native PR previews; app per PR via its GitHub action; envs in the app spec | [`digitalocean.app`](/reference/plan-format#digitalocean-app) | `DIGITALOCEAN_ACCESS_TOKEN` |
| [Firebase Hosting](/reference/providers#firebase-hosting) | Preview channel per PR (`pr-42`) that the preview is deployed into, with its own `web.app` URL; deleted when the PR closes instead of left to expire | recipe [`firebase_hosting.channel`](/reference/recipes#firebase_hosting-channel) | the token `gcloud auth print-access-token` prints (bearer) |

### Vercel

Env vars per target and git branch; wait for the preview deployment, redeploy if it built before the write.

* **Ops**: [`vercel.env`](/reference/plan-format#vercel-env), [`vercel.deploy`](/reference/plan-format#vercel-deploy)
* **Credential**: `VERCEL_TOKEN`
* **Token**: [create it](https://vercel.com/account/tokens): A token whose Scope is the team that owns the project, with an expiration. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `vercel`, `@vercel/*`, `vercel.json`, `.vercel/project.json`, `VERCEL_*`

```yaml theme={null}
# from examples/clerk-preview-callbacks.plan.yaml
providers:
  vercel: { project: prj_acme_web }
changes:
  - 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]
```

### Netlify

Env var values per deploy context (`deploy-preview`, `branch` + `context_parameter`); wait for the deploy of the commit, rebuild if it started before the write.

* **Ops**: [`netlify.env`](/reference/plan-format#netlify-env)
* **Credential**: `NETLIFY_AUTH_TOKEN`
* **Token**: [create it](https://app.netlify.com/user/applications#personal-access-tokens): A personal access token (Netlify tokens have no scopes) with an expiration; allow access to a SAML-protected team if yours enforces SSO. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@netlify/*`, `netlify-cli`, `netlify.toml`, `.netlify/state.json`, `NETLIFY_*`

```yaml theme={null}
# from examples/netlify-neon-preview.plan.yaml
providers:
  netlify: { site: 3f1c2a9e-0b7d-4e55-9c1a-acme00000001 }
changes:
  - id: env
    adapter: netlify
    op: env
    values:
      DATABASE_URL: { from: db.connection_string }
    environments: [preview]
```

### Cloudflare Pages / Workers

Pages: `deployment_configs.preview.env_vars` (all previews share one set, no per-branch values). Workers: secrets per script / preview version (Pages variables; Workers secrets are not covered).

* **Ops**: [`cloudflare.pages_env`](/reference/plan-format#cloudflare-pages_env)
* **Credential**: `CLOUDFLARE_API_TOKEN`
* **Token**: [create it](https://dash.cloudflare.com/profile/api-tokens): A custom token with Account > Cloudflare Pages > Edit, limited to the one account. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `wrangler`, `@cloudflare/next-on-pages`, `wrangler.json`, `wrangler.jsonc`, `wrangler.toml`, `CLOUDFLARE_*`

```yaml theme={null}
# from examples/cloudflare-pages-vars.plan.yaml
providers:
  cloudflare: { account: 0123456789abcdef0123456789abcdef, project: acme-web }
changes:
  - 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]
```

### Render

Env vars on a preview service / preview environment (per variable on a service whose id the plan gives (Render creates previews itself; no lookup of PR N's service); writing does not deploy).

* **Ops**: recipe [`render.env_var`](/reference/recipes#render-env_var)
* **Credential**: `RENDER_API_KEY` (bearer)
* **Token**: [create it](https://dashboard.render.com/u/settings?add-api-key): An API key; Render keys have no scopes and reach every workspace of the user who creates them. `sponson init --setup` checks it with `GET /owners`.
* **Detected by `sponson init` from**: `render.yaml`, `RENDER_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: env_var
  adapter: http
  op: resource
  recipe: render.env_var
  service_id: srv-d0a1b2c3d4e5f6g7h8i9
  key: API_URL
  value: https://api-${ctx.scope}.example.com
```

### Railway

Variables in a PR environment (Railway creates PR environments natively) (the PR environment's id is a param; Sponson does not wait for the redeploy a write triggers).

* **Ops**: recipe [`railway.variable`](/reference/recipes#railway-variable)
* **Credential**: `RAILWAY_API_TOKEN` (bearer)
* **Token**: [create it](https://railway.com/account/tokens): A workspace token for the workspace that holds the project. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `railway.json`, `railway.toml`, `RAILWAY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: variable
  adapter: http
  op: resource
  recipe: railway.variable
  project_id: prj-railway-demo
  environment_id: env-pr-42
  service_id: svc-web
  name: API_URL
  value: https://api-${ctx.scope}.example.app
```

### Fly.io

Per-PR app (the usual pattern is one app per PR) plus its secrets (secrets before the PR's deploy; a later change needs `fly secrets deploy`).

* **Ops**: recipe [`fly_machines.app`](/reference/recipes#fly_machines-app), recipe [`fly.secret`](/reference/recipes#fly-secret)
* **Credential**: `FLY_API_TOKEN` (bearer)
* **Token**: [create it](https://fly.io/dashboard): An org token (`fly tokens create org`): it creates and deletes apps in the organization. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Token**: [create it](https://fly.io/dashboard): A deploy token for the one app (`fly tokens create deploy -a <app>`), or an org token (`fly tokens create org`) for an app per PR. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `fly.toml`, `FLY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: app
  adapter: http
  op: resource
  recipe: fly_machines.app
  app_name: demo-${ctx.scope}
  org_slug: acme
```

### Heroku

Review app per PR; config vars on it (config vars on an existing app (a review app by name); creating the review app (async, by Heroku's automatic review apps) is not managed).

* **Ops**: recipe [`heroku.config_var`](/reference/recipes#heroku-config_var)
* **Credential**: `HEROKU_API_KEY` (bearer)
* **Token**: [create it](https://dashboard.heroku.com/account/applications): A long-lived authorization from `heroku authorizations:create -s write-protected`: the narrowest scope that reads and writes config vars. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `heroku.yml`, `HEROKU_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: config_var
  adapter: http
  op: list_item
  recipe: heroku.config_var
  app: myapp-${ctx.scope}
  key: API_URL
  value: https://api-${ctx.scope}.example.com
```

### AWS Amplify

Branch (per-PR previews built in); env vars per branch (variables before the branch's first build (no rebuild)).

* **Ops**: recipe [`amplify.branch`](/reference/recipes#amplify-branch), recipe [`amplify.branch_variable`](/reference/recipes#amplify-branch_variable)
* **Credential**: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (AWS SigV4, `amplify` in `us-east-1`)
* **Token**: [create it](https://console.aws.amazon.com/iam/home#/users): An IAM user's access key whose policy allows amplify:ListApps, amplify:GetBranch, amplify:CreateBranch, amplify:UpdateBranch and amplify:DeleteBranch on the app. `sponson init --setup` checks it with `GET /apps`.
* **Detected by `sponson init` from**: `amplify.yml`

```yaml theme={null}
# under changes: (no provider block needed)
- id: branch
  adapter: http
  op: resource
  recipe: amplify.branch
  app_id: d1demo
  branch_name: ${ctx.scope}
  stage: PULL_REQUEST
  description: Preview of ${ctx.scope}
```

### Google Cloud Run

Tagged revision per PR (`--tag pr-42`), env vars on it (a revision of an existing service per scope from its template, with the plan's image and variables, tagged and given no traffic (traffic on LATEST is pinned only with the line's `pin_latest: true`, refused otherwise); destroy removes the tag and Cloud Run deletes the revision later).

* **Ops**: [`cloud_run.revision`](/reference/plan-format#cloud_run-revision)
* **Credential**: the token `gcloud auth print-access-token` prints (bearer)
* **Token**: [set up access](https://console.cloud.google.com/iam-admin/iam): Sign in with `gcloud auth login` as an identity holding Cloud Run Developer (roles/run.developer) and Service Account User on the service's runtime service account. `sponson init --setup` shows this; there is no token to store.
* **Detected by `sponson init` from**: `@google-cloud/run`, `CLOUD_RUN_*`, `K_SERVICE`

```yaml theme={null}
# from examples/cloud-run-neon-preview.plan.yaml
providers:
  cloud_run: { project: acme-web-prod, region: europe-west1 }
changes:
  - 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]
```

### Azure Static Web Apps

Preview environment per PR (built in); app settings (after the PR's first deploy (the environment exists only then)).

* **Ops**: recipe [`azure_swa.build_app_setting`](/reference/recipes#azure_swa-build_app_setting)
* **Credential**: the token `az account get-access-token --query accessToken -o tsv` prints (bearer)
* **Token**: [set up access](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles): Sign in with `az login` as an identity holding Contributor on the static web app (or a custom role with Microsoft.Web/staticSites/\*). `sponson init --setup` shows this; there is no token to store.
* **Detected by `sponson init` from**: `staticwebapp.config.json`, `AZURE_STATIC_WEB_APPS_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: build_app_setting
  adapter: http
  op: list_item
  recipe: azure_swa.build_app_setting
  subscription_id: 00000000-0000-0000-0000-000000000000
  resource_group: rg-web
  site: web
  environment: "42"
  name: API_URL
  value: https://api-${ctx.scope}.example.app
```

### DigitalOcean App Platform

No native PR previews; app per PR via its GitHub action; envs in the app spec (an app per scope from a whole spec, waited for until deployed (in place of the app DigitalOcean's GitHub action makes); secret values are not compared).

* **Ops**: [`digitalocean.app`](/reference/plan-format#digitalocean-app)
* **Credential**: `DIGITALOCEAN_ACCESS_TOKEN`
* **Token**: [create it](https://cloud.digitalocean.com/account/api/tokens): A personal access token with Custom Scopes: app:read and app:update. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `.do/app.yaml`, `.do/deploy.template.yaml`, `DIGITALOCEAN_*`

```yaml theme={null}
# from scenarios/adapters/digitalocean.yaml
changes:
  - id: web
    adapter: digitalocean
    op: app
    spec:
      name: web-${ctx.scope}
      services:
        - name: web
          github: { repo: acme/web, branch: "${ctx.git.branch}" }
      envs:
        - key: API_URL
          value: https://api-${ctx.scope}.example.com
          scope: RUN_AND_BUILD_TIME
        - key: DATABASE_URL
          value: { secret: env://DO_SCENARIO_DB_URL }
          type: SECRET
    environments: [preview]
```

### Firebase Hosting

Preview channel per PR (`pr-42`) that the preview is deployed into, with its own `web.app` URL; deleted when the PR closes instead of left to expire (the channel only: deploying into it stays `firebase hosting:channel:deploy` (which also authorizes its domain in Firebase Auth); its `url` is an output).

* **Ops**: recipe [`firebase_hosting.channel`](/reference/recipes#firebase_hosting-channel)
* **Credential**: the token `gcloud auth print-access-token` prints (bearer)
* **Token**: [set up access](https://firebase.google.com/docs/projects/iam/roles-predefined-product): Sign in with `gcloud auth login` (or activate a service account) as an identity holding Firebase Hosting Admin (roles/firebasehosting.admin) on the project. `sponson init --setup` shows this; there is no token to store.
* **Detected by `sponson init` from**: `.github/workflows/firebase-hosting-pull-request.yml`

```yaml theme={null}
# under changes: (no provider block needed)
- id: channel
  adapter: http
  op: resource
  recipe: firebase_hosting.channel
  site: demo-project
  channel_id: ${ctx.scope}
```

## Databases with branching or per-PR instances

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Neon](/reference/providers#neon) | Copy-on-write branch per PR, connection string out | [`neon.branch`](/reference/plan-format#neon-branch) | `NEON_API_KEY` |
| [Supabase](/reference/providers#supabase) | Preview branch per PR (a separate project with its own ref, keys and URL) | [`supabase.branch`](/reference/plan-format#supabase-branch) | `SUPABASE_ACCESS_TOKEN` |
| [PlanetScale](/reference/providers#planetscale) | Branch per PR; password (credential) per branch; deploy request on merge | [`planetscale.branch`](/reference/plan-format#planetscale-branch), [`planetscale.password`](/reference/plan-format#planetscale-password) | `PLANETSCALE_SERVICE_TOKEN_ID` + `PLANETSCALE_SERVICE_TOKEN` |
| [Turso](/reference/providers#turso) | Database branched from a parent (`seed: {type: database}`), token per DB | recipe [`turso.database_branch`](/reference/recipes#turso-database_branch) | `TURSO_API_TOKEN` (bearer) |
| [Xata](/reference/providers#xata) | Copy-on-write Postgres branch | [`xata.branch`](/reference/plan-format#xata-branch) | `XATA_API_KEY` |
| [MongoDB Atlas](/reference/providers#mongodb-atlas) | No branching; per-PR database user and database name on a shared cluster (cluster per PR is slow and costly) | recipe [`mongodb_atlas.database_user`](/reference/recipes#mongodb_atlas-database_user) | `MONGODB_ATLAS_PUBLIC_API_KEY` / `MONGODB_ATLAS_PRIVATE_API_KEY` (HTTP Digest) |
| [CockroachDB Cloud](/reference/providers#cockroachdb-cloud) | No branching; per-PR database is SQL (`CREATE DATABASE`), a cluster per PR is infra | recipe [`cockroachdb.database`](/reference/recipes#cockroachdb-database), recipe [`cockroachdb.sql_user`](/reference/recipes#cockroachdb-sql_user) | `COCKROACH_API_KEY` (bearer) |
| [Prisma Postgres](/reference/providers#prisma-postgres) | Database per PR in a project, connection string per database | recipe [`prisma.database`](/reference/recipes#prisma-database), recipe [`prisma.database_copy`](/reference/recipes#prisma-database_copy) | `PRISMA_SERVICE_TOKEN` (bearer) |

### Neon

Copy-on-write branch per PR, connection string out.

* **Ops**: [`neon.branch`](/reference/plan-format#neon-branch)
* **Credential**: `NEON_API_KEY`
* **Token**: [create it](https://neon.com/docs/manage/api-keys): A project-scoped API key for the one project (an organization admin creates it); otherwise a personal API key. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@neondatabase/*`, `@prisma/adapter-neon`, `neonctl`, `.neon`, `NEON_*`, database URL on `*.neon.tech`

```yaml theme={null}
# from examples/adopting-an-existing-project.plan.yaml
providers:
  neon: { project: proj_acme_db }
changes:
  - id: db-staging
    adapter: neon
    op: branch
    name: staging
    environments: [preview]
```

### Supabase

Preview branch per PR (a separate project with its own ref, keys and URL).

* **Ops**: [`supabase.branch`](/reference/plan-format#supabase-branch)
* **Credential**: `SUPABASE_ACCESS_TOKEN`
* **Token**: [create it](https://supabase.com/dashboard/account/tokens): A personal access token limited to the project, with Development Branches, Auth Config and Project Settings set to Read-write. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@supabase/*`, `supabase/config.toml`, `supabase/.temp/project-ref`, `SUPABASE_*`, database URL on `*.supabase.co`, database URL on `*.supabase.com`

```yaml theme={null}
# from examples/nextjs-supabase-preview.plan.yaml
providers:
  supabase: { project: acmewebprojectrefxyz }
changes:
  - id: db
    adapter: supabase
    op: branch
    environments: [preview]
```

### PlanetScale

Branch per PR; password (credential) per branch; deploy request on merge (deploy requests on merge are not managed).

* **Ops**: [`planetscale.branch`](/reference/plan-format#planetscale-branch), [`planetscale.password`](/reference/plan-format#planetscale-password)
* **Credential**: `PLANETSCALE_SERVICE_TOKEN_ID` + `PLANETSCALE_SERVICE_TOKEN`
* **Token**: [create it](https://planetscale.com/docs/api/reference/service-tokens): A service token with create\_branch, delete\_branch, read\_branch and connect\_branch (and delete\_branch\_password) on the one database. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@planetscale/*`, `PLANETSCALE_*`, database URL on `*.psdb.cloud`

```yaml theme={null}
# from examples/nextjs-planetscale-preview.plan.yaml
providers:
  planetscale: { organization: acme, database: acme-db }
changes:
  - id: db
    adapter: planetscale
    op: branch
    parent: main
    environments: [preview]
```

### Turso

Database branched from a parent (`seed: {type: database}`), token per DB.

* **Ops**: recipe [`turso.database_branch`](/reference/recipes#turso-database_branch)
* **Credential**: `TURSO_API_TOKEN` (bearer)
* **Token**: [create it](https://docs.turso.tech/api-reference/authentication): A platform API token for the organization (`turso auth api-tokens mint <name> --org <org>`). `sponson init --setup` checks it with `GET /auth/validate`.
* **Detected by `sponson init` from**: `@libsql/client`, `@tursodatabase/*`, `TURSO_*`, database URL on `*.turso.io`

```yaml theme={null}
# under changes: (no provider block needed)
- id: database_branch
  adapter: http
  op: resource
  recipe: turso.database_branch
  organization: acme
  name: app-${ctx.scope}
  parent: app
```

### Xata

Copy-on-write Postgres branch (waits until the branch is healthy; only its existence is compared for drift).

* **Ops**: [`xata.branch`](/reference/plan-format#xata-branch)
* **Credential**: `XATA_API_KEY`
* **Token**: [create it](https://xata.io/docs/platform/api-key.md): An API key limited to the project, with Branch: Read and Write. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@xata.io/*`, `.xatarc`, `XATA_*`

```yaml theme={null}
# from scenarios/adapters/xata.yaml
providers:
  xata: { organization: acme, project: app }
changes:
  - id: db
    adapter: xata
    op: branch
    name: ${ctx.scope}
    parent_id: br_main
    environments: [preview]
```

### MongoDB Atlas

No branching; per-PR database user and database name on a shared cluster (cluster per PR is slow and costly) (the password is an input (`{ secret }`, or a sensitive output of another line), sent on create only and never compared).

* **Ops**: recipe [`mongodb_atlas.database_user`](/reference/recipes#mongodb_atlas-database_user)
* **Credential**: `MONGODB_ATLAS_PUBLIC_API_KEY` / `MONGODB_ATLAS_PRIVATE_API_KEY` (HTTP Digest)
* **Token**: [create it](https://www.mongodb.com/docs/atlas/configure-api-access-project/): A project API key with Project Network Access Manager (access list) or Project Database Access Admin (database users), and an API access list entry. `sponson init --setup` checks it with `GET /groups`.
* **Detected by `sponson init` from**: `MONGODB_ATLAS_*`, database URL on `*.mongodb.net`

```yaml theme={null}
# under changes: (no provider block needed)
- id: database_user
  adapter: http
  op: resource
  recipe: mongodb_atlas.database_user
  group_id: 32b6e34b3d91647abb20e7b8
  username: app-${ctx.scope}
  password:
    secret: env://PREVIEW_DB_PASSWORD
  roles:
    - databaseName: app_${ctx.scope}
      roleName: readWrite
```

### CockroachDB Cloud

No branching; per-PR database is SQL (`CREATE DATABASE`), a cluster per PR is infra (the Cloud API creates databases and SQL users (`cockroachdb.sql_user`, which gets the SQL `admin` role by default); grants narrower than that still need SQL).

* **Ops**: recipe [`cockroachdb.database`](/reference/recipes#cockroachdb-database), recipe [`cockroachdb.sql_user`](/reference/recipes#cockroachdb-sql_user)
* **Credential**: `COCKROACH_API_KEY` (bearer)
* **Token**: [create it](https://cockroachlabs.cloud/access): A service account (Access Management > Service Accounts) with the Cluster Admin role on the cluster, then Create API Key. `sponson init --setup` checks it with `GET /clusters`.
* **Detected by `sponson init` from**: `COCKROACH_*`, database URL on `*.cockroachlabs.cloud`

```yaml theme={null}
# under changes: (no provider block needed)
- id: database
  adapter: http
  op: resource
  recipe: cockroachdb.database
  cluster_id: 9e3f1a52-63d1-4a4c-9b8f-2c8b7a0e5d11
  name: app_${ctx.scope}
```

### Prisma Postgres

Database per PR in a project, connection string per database (connection strings are once-only outputs (in the create answer only); Sponson does not wait for status ready).

* **Ops**: recipe [`prisma.database`](/reference/recipes#prisma-database), recipe [`prisma.database_copy`](/reference/recipes#prisma-database_copy)
* **Credential**: `PRISMA_SERVICE_TOKEN` (bearer)
* **Token**: [create it](https://console.prisma.io/): A workspace service token (Workspace Settings > Service Tokens); it covers the whole workspace, with no finer scopes. `sponson init --setup` checks it with `GET /projects`.
* **Detected by `sponson init` from**: `@prisma/ppg`, database URL on `*.prisma.io`, database URL on `*.prisma-data.net`

```yaml theme={null}
# under changes: (no provider block needed)
- id: database
  adapter: http
  op: resource
  recipe: prisma.database
  project_id: proj_cm1a2b3c4d5e6f7g8h9
  name: app-${ctx.scope}
```

## Auth callback and redirect allow-lists

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Clerk](/reference/providers#clerk) | Preview URL on the instance's redirect allow-list | [`clerk.redirect_allow`](/reference/plan-format#clerk-redirect_allow) | `CLERK_SECRET_KEY` |
| [Auth0](/reference/providers#auth0) | Preview URL in an application's `callbacks`, `allowed_logout_urls`, `web_origins`, `allowed_origins` | recipe [`auth0.callback_url`](/reference/recipes#auth0-callback_url), recipe [`auth0.logout_url`](/reference/recipes#auth0-logout_url), recipe [`auth0.web_origin`](/reference/recipes#auth0-web_origin), recipe [`auth0.allowed_origin`](/reference/recipes#auth0-allowed_origin) | `AUTH0_CLIENT_ID` / `AUTH0_CLIENT_SECRET` (OAuth2 client credentials) |
| [Supabase Auth](/reference/providers#supabase-auth) | Preview URL in "Additional Redirect URLs" | [`supabase.auth_redirect`](/reference/plan-format#supabase-auth_redirect) | `SUPABASE_ACCESS_TOKEN` |
| [Firebase Auth](/reference/providers#firebase-auth) | Preview domain in `authorizedDomains` | recipe [`firebase.authorized_domain`](/reference/recipes#firebase-authorized_domain) | the token `gcloud auth print-access-token` prints (bearer) |
| [WorkOS](/reference/providers#workos) | Redirect URI for AuthKit | recipe [`workos.redirect_uri`](/reference/recipes#workos-redirect_uri) | `WORKOS_API_KEY` (bearer) |
| [Stytch](/reference/providers#stytch) | Redirect URL per environment | recipe [`stytch.redirect_url`](/reference/recipes#stytch-redirect_url) | `STYTCH_WORKSPACE_KEY_ID` / `STYTCH_WORKSPACE_KEY_SECRET` (basic) |
| [Okta](/reference/providers#okta) | `redirect_uris` on an OIDC app; trusted origin for CORS | recipe [`okta.redirect_uri`](/reference/recipes#okta-redirect_uri), recipe [`okta.post_logout_redirect_uri`](/reference/recipes#okta-post_logout_redirect_uri), recipe [`okta.trusted_origin`](/reference/recipes#okta-trusted_origin) | `OKTA_API_TOKEN` (header `Authorization`, after `SSWS`) |
| [Kinde](/reference/providers#kinde) | Callback and logout URLs on an application | recipe [`kinde.callback_url`](/reference/recipes#kinde-callback_url), recipe [`kinde.logout_url`](/reference/recipes#kinde-logout_url) | `KINDE_MANAGEMENT_CLIENT_ID` / `KINDE_MANAGEMENT_CLIENT_SECRET` (OAuth2 client credentials) |
| [Google OAuth clients](/reference/providers#google-oauth-clients) | Preview URL in a web client's authorized redirect URIs | [`manual.step`](/reference/plan-format#manual-step) | none (a person does it) |

### Clerk

Preview URL on the instance's redirect allow-list.

* **Ops**: [`clerk.redirect_allow`](/reference/plan-format#clerk-redirect_allow)
* **Credential**: `CLERK_SECRET_KEY`
* **Token**: [create it](https://dashboard.clerk.com/last-active?path=api-keys): The Secret Key of the instance whose redirect URLs the plan manages (sk\_test\_ for development); Clerk secret keys have no scopes. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@clerk/*`, `CLERK_*`

```yaml theme={null}
# from examples/clerk-preview-callbacks.plan.yaml
changes:
  - id: callback
    adapter: clerk
    op: redirect_allow
    url: { from: env.preview_url }
    environments: [preview]
```

### Auth0

Preview URL in an application's `callbacks`, `allowed_logout_urls`, `web_origins`, `allowed_origins`.

* **Ops**: recipe [`auth0.callback_url`](/reference/recipes#auth0-callback_url), recipe [`auth0.logout_url`](/reference/recipes#auth0-logout_url), recipe [`auth0.web_origin`](/reference/recipes#auth0-web_origin), recipe [`auth0.allowed_origin`](/reference/recipes#auth0-allowed_origin)
* **Credential**: `AUTH0_CLIENT_ID` / `AUTH0_CLIENT_SECRET` (OAuth2 client credentials)
* **Token**: [create it](https://manage.auth0.com/#/applications): A Machine to Machine application authorized for the Auth0 Management API with only the read:clients and update:clients scopes. `sponson init --setup` checks it with `GET /clients`.
* **Detected by `sponson init` from**: `auth0`, `@auth0/*`, `AUTH0_*`

```yaml theme={null}
providers:
  http:
    auth0:
      recipe: auth0
      base_url: https://acme.eu.auth0.com/api/v2
changes:
  - id: callback_url
    adapter: http
    op: list_item
    recipe: auth0.callback_url
    client_id: AbC123dEf456GhI789jKl012MnO345pQ
    url: https://${ctx.scope}.preview.example.app/callback
```

### Supabase Auth

Preview URL in "Additional Redirect URLs".

* **Ops**: [`supabase.auth_redirect`](/reference/plan-format#supabase-auth_redirect)
* **Credential**: `SUPABASE_ACCESS_TOKEN`
* **Token**: [create it](https://supabase.com/dashboard/account/tokens): A personal access token limited to the project, with Development Branches, Auth Config and Project Settings set to Read-write. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@supabase/*`, `supabase/config.toml`, `supabase/.temp/project-ref`, `SUPABASE_*`, database URL on `*.supabase.co`, database URL on `*.supabase.com`

```yaml theme={null}
# from examples/nextjs-supabase-preview.plan.yaml
providers:
  supabase: { project: acmewebprojectrefxyz }
changes:
  - id: auth_callback
    adapter: supabase
    op: auth_redirect
    project: { from: db.project_ref }
    url: { from: env.preview_url }
    environments: [preview]
```

### Firebase Auth

Preview domain in `authorizedDomains`.

* **Ops**: recipe [`firebase.authorized_domain`](/reference/recipes#firebase-authorized_domain)
* **Credential**: the token `gcloud auth print-access-token` prints (bearer)
* **Token**: [set up access](https://firebase.google.com/docs/projects/iam/roles-predefined-product): Sign in with `gcloud auth login` as an identity holding Firebase Authentication Admin (roles/firebaseauth.admin) on the project. `sponson init --setup` shows this; there is no token to store.
* **Detected by `sponson init` from**: `firebase`, `firebase-admin`, `firebase.json`, `.firebaserc`, `FIREBASE_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: authorized_domain
  adapter: http
  op: list_item
  recipe: firebase.authorized_domain
  project: demo-project
  domain: ${ctx.scope}.preview.example.app
```

### WorkOS

Redirect URI for AuthKit.

* **Ops**: recipe [`workos.redirect_uri`](/reference/recipes#workos-redirect_uri)
* **Credential**: `WORKOS_API_KEY` (bearer)
* **Token**: [create it](https://dashboard.workos.com/api-keys): The secret API key of the environment (staging for previews); WorkOS keys have no finer permissions. `sponson init --setup` checks it with `GET /organizations`.
* **Detected by `sponson init` from**: `@workos-inc/*`, `WORKOS_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: redirect_uri
  adapter: http
  op: resource
  recipe: workos.redirect_uri
  uri: https://${ctx.scope}.preview.example.com/callback
```

### Stytch

Redirect URL per environment.

* **Ops**: recipe [`stytch.redirect_url`](/reference/recipes#stytch-redirect_url)
* **Credential**: `STYTCH_WORKSPACE_KEY_ID` / `STYTCH_WORKSPACE_KEY_SECRET` (basic)
* **Token**: [create it](https://stytch.com/dashboard/settings/management-api): A Workspace Management API key (Workspace settings > Management API); its key ID and secret, no finer scopes. `sponson init --setup` checks it with `GET /pwa/v3/projects`.
* **Detected by `sponson init` from**: `stytch`, `@stytch/*`, `STYTCH_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: redirect_url
  adapter: http
  op: resource
  recipe: stytch.redirect_url
  project_slug: my-app
  environment_slug: test
  url: https://${ctx.scope}.preview.example.com/authenticate
```

### Okta

`redirect_uris` on an OIDC app; trusted origin for CORS.

* **Ops**: recipe [`okta.redirect_uri`](/reference/recipes#okta-redirect_uri), recipe [`okta.post_logout_redirect_uri`](/reference/recipes#okta-post_logout_redirect_uri), recipe [`okta.trusted_origin`](/reference/recipes#okta-trusted_origin)
* **Credential**: `OKTA_API_TOKEN` (header `Authorization`, after `SSWS`)
* **Token**: [create it](https://developer.okta.com/docs/guides/create-an-api-token/main/): An API token (Security > API > Tokens) created by an admin whose role can edit the app (it acts with that admin's permissions). `sponson init --setup` checks it with `GET /users/me`.
* **Detected by `sponson init` from**: `@okta/*`, `OKTA_*`

```yaml theme={null}
providers:
  http:
    okta:
      recipe: okta
      base_url: https://acme.okta.com/api/v1
changes:
  - id: redirect_uri
    adapter: http
    op: list_item
    recipe: okta.redirect_uri
    app_id: 0oa1gjh63g214q0Hq0g4
    url: https://${ctx.scope}.preview.example.app/login/callback
```

### Kinde

Callback and logout URLs on an application.

* **Ops**: recipe [`kinde.callback_url`](/reference/recipes#kinde-callback_url), recipe [`kinde.logout_url`](/reference/recipes#kinde-logout_url)
* **Credential**: `KINDE_MANAGEMENT_CLIENT_ID` / `KINDE_MANAGEMENT_CLIENT_SECRET` (OAuth2 client credentials)
* **Token**: [create it](https://docs.kinde.com/developer-tools/kinde-api/connect-to-kinde-api/): A Machine to Machine application authorized for the Kinde Management API with only the application redirect and logout URI scopes. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `@kinde-oss/*`, `KINDE_*`

```yaml theme={null}
providers:
  http:
    kinde:
      recipe: kinde
      base_url: https://acme.kinde.com/api/v1
changes:
  - id: callback_url
    adapter: http
    op: list_item
    recipe: kinde.callback_url
    app_id: 3b0b5c6c8fcc464fab397f4969b5f482
    url: https://${ctx.scope}.preview.example.app/api/auth/kinde_callback
```

### Google OAuth clients

Preview URL in a web client's authorized redirect URIs.

* **Ops**: [`manual.step`](/reference/plan-format#manual-step)
* **Credential**: none (a person does it)
* **Detected by `sponson init` from**: not detected (write the line yourself)

```yaml theme={null}
# from examples/google-oauth-manual-step.plan.yaml
changes:
  - 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]
```

## Feature flags and targeting

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [LaunchDarkly](/reference/providers#launchdarkly) | Flag on for a preview: target a context key or add a rule on `url`, in a per-PR or shared preview environment | [`launchdarkly.flag_target`](/reference/plan-format#launchdarkly-flag_target) | `LAUNCHDARKLY_ACCESS_TOKEN` |
| [PostHog](/reference/providers#posthog) | Release condition matching the preview host on a flag | recipe [`posthog.release_condition`](/reference/recipes#posthog-release_condition) | `POSTHOG_PERSONAL_API_KEY` (bearer) |
| [Statsig](/reference/providers#statsig) | Gate rule for the preview (condition `url` or `environment_tier`) | recipe [`statsig.gate_rule`](/reference/recipes#statsig-gate_rule) | `STATSIG_CONSOLE_API_KEY` (header `STATSIG-API-KEY`) |
| [GrowthBook](/reference/providers#growthbook) | Force rule in a per-environment feature | recipe [`growthbook.force_rule`](/reference/recipes#growthbook-force_rule) | `GROWTHBOOK_API_KEY` (bearer) |
| [Unleash](/reference/providers#unleash) | Strategy (with constraints) on a flag in an environment | recipe [`unleash.flag_strategy`](/reference/recipes#unleash-flag_strategy) | `UNLEASH_AUTH_TOKEN` (header `Authorization`) |
| [Flagsmith](/reference/providers#flagsmith) | Segment override or a per-PR environment | recipe [`flagsmith.environment`](/reference/recipes#flagsmith-environment) | `FLAGSMITH_MASTER_API_KEY` (header `Authorization`, after `Api-Key`) |
| [ConfigCat](/reference/providers#configcat) | Targeting rule on a setting in an environment | recipe [`configcat.targeting_rule`](/reference/recipes#configcat-targeting_rule) | `CONFIGCAT_BASIC_AUTH_USERNAME` / `CONFIGCAT_BASIC_AUTH_PASSWORD` (basic) |
| [Split (Harness FME)](/reference/providers#split-harness-fme) | Targeting rule / individual target in an environment | recipe [`split.targeting_rule`](/reference/recipes#split-targeting_rule) | `SPLIT_ADMIN_API_KEY` (bearer) |

### LaunchDarkly

Flag on for a preview: target a context key or add a rule on `url`, in a per-PR or shared preview environment (context-key targets; `url` rules not yet).

* **Ops**: [`launchdarkly.flag_target`](/reference/plan-format#launchdarkly-flag_target)
* **Credential**: `LAUNCHDARKLY_ACCESS_TOKEN`
* **Token**: [create it](https://app.launchdarkly.com/settings/authorization): An access token whose custom role allows updateTargets on the environment's flags and viewProject (without custom roles: the Writer role). `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `@launchdarkly/*`, `launchdarkly-node-server-sdk`, `launchdarkly-js-client-sdk`, `launchdarkly-react-client-sdk`, `LAUNCHDARKLY_*`

```yaml theme={null}
# from examples/launchdarkly-preview-flags.plan.yaml
providers:
  launchdarkly: { project: acme-web, environment: preview }
changes:
  - id: checkout
    adapter: launchdarkly
    op: flag_target
    flag: new-checkout
    context_kind: url
    key: { from: env.preview_url }
    variation: true
    environments: [preview]
```

### PostHog

Release condition matching the preview host on a flag (condition on a person or group property; needs the merging `filters` PATCH of current PostHog (PH5)).

* **Ops**: recipe [`posthog.release_condition`](/reference/recipes#posthog-release_condition)
* **Credential**: `POSTHOG_PERSONAL_API_KEY` (bearer)
* **Token**: [create it](https://us.posthog.com/settings/user-api-keys): A personal API key with only the feature\_flag:read and feature\_flag:write scopes, limited to the project (EU: eu.posthog.com). `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `posthog-js`, `posthog-node`, `posthog-react-native`, `POSTHOG_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: release_condition
  adapter: http
  op: list_item
  recipe: posthog.release_condition
  project_id: "12345"
  flag_id: "678"
  property: $host
  value: ${ctx.scope}.preview.example.com
```

### Statsig

Gate rule for the preview (condition `url` or `environment_tier`) (gates requiring reviews: write behaviour undocumented (SG8)).

* **Ops**: recipe [`statsig.gate_rule`](/reference/recipes#statsig-gate_rule)
* **Credential**: `STATSIG_CONSOLE_API_KEY` (header `STATSIG-API-KEY`)
* **Token**: [create it](https://console.statsig.com/api_keys): A Console API key (Project Settings > API Keys) that can edit gates. `sponson init --setup` checks it with `GET /gates`.
* **Detected by `sponson init` from**: `statsig-js`, `statsig-node`, `statsig-react`, `@statsig/*`, `STATSIG_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: gate_rule
  adapter: http
  op: resource
  recipe: statsig.gate_rule
  gate_id: new_checkout
  name: preview ${ctx.scope}
  field: preview
  values:
    - ${ctx.scope}
```

### GrowthBook

Force rule in a per-environment feature (v2 API (rules with per-rule environments); approval-gated features refuse the write).

* **Ops**: recipe [`growthbook.force_rule`](/reference/recipes#growthbook-force_rule)
* **Credential**: `GROWTHBOOK_API_KEY` (bearer)
* **Token**: [create it](https://app.growthbook.io/settings/keys): A Secret API key (Settings > API Keys) with a role that can edit features, limited to the project. `sponson init --setup` checks it with `GET /v1/features`.
* **Detected by `sponson init` from**: `@growthbook/*`, `GROWTHBOOK_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: force_rule
  adapter: http
  op: list_item
  recipe: growthbook.force_rule
  feature_id: new-checkout
  environment: staging
  description: Sponson preview ${ctx.scope}
  condition: '{"preview": "${ctx.scope}"}'
  value: "true"
```

### Unleash

Strategy (with constraints) on a flag in an environment (deleting the last strategy turns the flag off in that environment (UN8)).

* **Ops**: recipe [`unleash.flag_strategy`](/reference/recipes#unleash-flag_strategy)
* **Credential**: `UNLEASH_AUTH_TOKEN` (header `Authorization`)
* **Token**: [create it](https://docs.getunleash.io/concepts/api-tokens-and-client-keys): A personal access token (or an Enterprise service account token) whose role edits flags and strategies in the project; not a client or frontend token. `sponson init --setup` checks it with `GET /api/admin/projects`.
* **Detected by `sponson init` from**: `unleash-client`, `unleash-proxy-client`, `@unleash/*`, `UNLEASH_*`

```yaml theme={null}
providers:
  http:
    unleash:
      recipe: unleash
      base_url: https://unleash.example.com
changes:
  - id: flag_strategy
    adapter: http
    op: resource
    recipe: unleash.flag_strategy
    feature: new-checkout
    environment: development
    title: preview ${ctx.scope}
    context_name: preview
    values:
      - ${ctx.scope}
```

### Flagsmith

Segment override or a per-PR environment (a per-PR environment (its client-side key is an output); identity and segment overrides are not recipes: Edge identities are addressed by UUID, and a segment override is two objects created in order).

* **Ops**: recipe [`flagsmith.environment`](/reference/recipes#flagsmith-environment)
* **Credential**: `FLAGSMITH_MASTER_API_KEY` (header `Authorization`, after `Api-Key`)
* **Token**: [create it](https://app.flagsmith.com/): An Organisation API key (Organisation Settings > API Keys); with role-based access, give it a role limited to the project. `sponson init --setup` checks it with `GET /organisations/`.
* **Detected by `sponson init` from**: `flagsmith`, `flagsmith-nodejs`, `react-native-flagsmith`, `FLAGSMITH_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: environment
  adapter: http
  op: resource
  recipe: flagsmith.environment
  project_id: "1234"
  name: preview-${ctx.scope}
```

### ConfigCat

Targeting rule on a setting in an environment (boolean flags; products requiring a change reason or approval refuse the write).

* **Ops**: recipe [`configcat.targeting_rule`](/reference/recipes#configcat-targeting_rule)
* **Credential**: `CONFIGCAT_BASIC_AUTH_USERNAME` / `CONFIGCAT_BASIC_AUTH_PASSWORD` (basic)
* **Token**: [create it](https://app.configcat.com/my-account/public-api-credentials): Public Management API credentials; they act with your user's product permissions, so use a member limited to the product. `sponson init --setup` checks it with `GET /v1/me`.
* **Detected by `sponson init` from**: `configcat-js`, `configcat-js-ssr`, `configcat-js-full`, `configcat-node`, `configcat-react`, `CONFIGCAT_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: targeting_rule
  adapter: http
  op: list_item
  recipe: configcat.targeting_rule
  environment_id: 08d8becf-d4d9-4c66-8b48-6ac74cd95fba
  setting_id: "12345"
  attribute: Preview
  match: ${ctx.scope}
```

### Split (Harness FME)

Targeting rule / individual target in an environment (targeting rule (not individual targets); projects requiring title/comment refuse the write).

* **Ops**: recipe [`split.targeting_rule`](/reference/recipes#split-targeting_rule)
* **Credential**: `SPLIT_ADMIN_API_KEY` (bearer)
* **Token**: [create it](https://developer.harness.io/docs/feature-management-experimentation/api-keys/): An Admin API key (Admin settings > API keys), restricted to the environments the lines change. `sponson init --setup` checks it with `GET /workspaces`.
* **Detected by `sponson init` from**: `@splitsoftware/*`, `SPLIT_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: targeting_rule
  adapter: http
  op: list_item
  recipe: split.targeting_rule
  workspace_id: a3b6f5d0-0e8f-11ee-9b0f-0242ac120002
  flag: new_checkout
  environment: Staging
  attribute: preview
  match: ${ctx.scope}
  treatment: on
```

## Webhooks registered per environment

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Stripe](/reference/providers#stripe) | Test-mode webhook endpoint pointing at the preview URL; its signing secret into the app's env | recipe [`stripe.webhook_endpoint`](/reference/recipes#stripe-webhook_endpoint) | `STRIPE_SECRET_KEY` (bearer) |
| [GitHub](/reference/providers#github) | Repository webhook to the preview URL | recipe [`github.repo_webhook`](/reference/recipes#github-repo_webhook) | `GITHUB_TOKEN` (bearer) |
| [Svix (as a sender)](/reference/providers#svix-as-a-sender) | Endpoint per environment on an application | recipe [`svix.endpoint`](/reference/recipes#svix-endpoint) | `SVIX_AUTH_TOKEN` (bearer) |
| [Clerk webhooks](/reference/providers#clerk-webhooks) | Preview URL as a webhook endpoint | [`manual.step`](/reference/plan-format#manual-step) | none (a person does it) |
| [Twilio](/reference/providers#twilio) | TwiML Application whose voice (and messaging, status) URLs are the preview's, named by the preview's Voice SDK tokens; a shared phone number has one URL and is not per-preview state | recipe [`twilio.twiml_app`](/reference/recipes#twilio-twiml_app) | `TWILIO_API_KEY` / `TWILIO_API_SECRET` (basic) |
| [Ably](/reference/providers#ably) | Integration rule (webhook) posting the preview's channels' messages or presence to the preview URL | recipe [`ably.webhook_rule`](/reference/recipes#ably-webhook_rule) | `ABLY_CONTROL_TOKEN` (bearer) |

### Stripe

Test-mode webhook endpoint pointing at the preview URL; its signing secret into the app's env.

* **Ops**: recipe [`stripe.webhook_endpoint`](/reference/recipes#stripe-webhook_endpoint)
* **Credential**: `STRIPE_SECRET_KEY` (bearer)
* **Token**: [create it](https://dashboard.stripe.com/test/apikeys): A test-mode restricted key (Create restricted key) with Webhook Endpoints set to Write and everything else None. `sponson init --setup` checks it with `GET /webhook_endpoints`.
* **Detected by `sponson init` from**: `stripe`, `@stripe/*`, `STRIPE_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: webhook_endpoint
  adapter: http
  op: resource
  recipe: stripe.webhook_endpoint
  name: preview-${ctx.scope}
  url: https://pr-42.preview.example.com
  enabled_events:
    - checkout.session.completed
    - customer.subscription.updated
  description: Preview webhook managed by Sponson
```

### GitHub

Repository webhook to the preview URL.

* **Ops**: recipe [`github.repo_webhook`](/reference/recipes#github-repo_webhook)
* **Credential**: `GITHUB_TOKEN` (bearer)
* **Token**: [create it](https://github.com/settings/personal-access-tokens/new): A fine-grained token for the one repository with the permission the line's op needs (Webhooks, Administration or Environments: Read and write). `sponson init --setup` checks it with `GET /user`.
* **Detected by `sponson init` from**: `@octokit/webhooks`, `probot`, `GITHUB_WEBHOOK_SECRET`

```yaml theme={null}
# under changes: (no provider block needed)
- id: repo_webhook
  adapter: http
  op: resource
  recipe: github.repo_webhook
  owner: acme
  repo: app
  url: https://${ctx.scope}.preview.example.com/api/github
```

### Svix (as a sender)

Endpoint per environment on an application.

* **Ops**: recipe [`svix.endpoint`](/reference/recipes#svix-endpoint)
* **Credential**: `SVIX_AUTH_TOKEN` (bearer)
* **Token**: [create it](https://dashboard.svix.com/api-access): The environment's API key (API Access); keys have no finer scopes. `sponson init --setup` checks it with `GET /api/v1/app`.
* **Detected by `sponson init` from**: `svix`, `SVIX_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: endpoint
  adapter: http
  op: resource
  recipe: svix.endpoint
  app_id: acme-customer
  uid: sponson-${ctx.env}-${ctx.scope}
  url: https://${ctx.scope}.preview.example.com/api/webhooks
```

### Clerk webhooks

Preview URL as a webhook endpoint.

* **Ops**: [`manual.step`](/reference/plan-format#manual-step)
* **Credential**: none (a person does it)
* **Detected by `sponson init` from**: not detected (write the line yourself)

```yaml theme={null}
# from examples/google-oauth-manual-step.plan.yaml
changes:
  - 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]
```

### Twilio

TwiML Application whose voice (and messaging, status) URLs are the preview's, named by the preview's Voice SDK tokens; a shared phone number has one URL and is not per-preview state (per-preview inbound SMS or calls need a number per preview (bought, A2P-registered for US SMS); a shared number's single URL is not managed, and subaccounts (closed, never deleted, numbers bought per subaccount) are not the model).

* **Ops**: recipe [`twilio.twiml_app`](/reference/recipes#twilio-twiml_app)
* **Credential**: `TWILIO_API_KEY` / `TWILIO_API_SECRET` (basic)
* **Token**: [create it](https://www.twilio.com/docs/iam/api-keys): A Standard API key (Console > Account > API keys & tokens); its SID goes in TWILIO\_API\_KEY and its secret in TWILIO\_API\_SECRET. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `twilio`, `TWILIO_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: twiml_app
  adapter: http
  op: resource
  recipe: twilio.twiml_app
  account_sid: ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  name: sponson-${ctx.env}-${ctx.scope}
  url: https://pr-42.preview.example.com
```

### Ably

Integration rule (webhook) posting the preview's channels' messages or presence to the preview URL (the app (shared) and its keys are parent objects; the preview's channels must carry a per-preview prefix for a filter to tell them apart).

* **Ops**: recipe [`ably.webhook_rule`](/reference/recipes#ably-webhook_rule)
* **Credential**: `ABLY_CONTROL_TOKEN` (bearer)
* **Token**: [create it](https://ably.com/docs/platform/account/control-api): An account access token (Account > My Access Tokens) with the `read:rule` and `write:rule` capabilities only. `sponson init --setup` checks it with `GET /me`.
* **Detected by `sponson init` from**: `ably`, `ABLY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: webhook_rule
  adapter: http
  op: resource
  recipe: ably.webhook_rule
  app_id: 28GY6a
  channel_filter: "^${ctx.scope}:"
  url: https://pr-42.preview.example.com
```

## Email: sending domains, webhooks, test inboxes

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Resend](/reference/providers#resend) | Restricted API key or webhook per environment (domains are long-lived; test addresses like `delivered@resend.dev` need no API) | recipe [`resend.api_key`](/reference/recipes#resend-api_key), recipe [`resend.webhook`](/reference/recipes#resend-webhook) | `RESEND_API_KEY` (bearer) |
| [Postmark](/reference/providers#postmark) | Server per environment, webhook per server | recipe [`postmark.server`](/reference/recipes#postmark-server), recipe [`postmark_server.webhook`](/reference/recipes#postmark_server-webhook) | `POSTMARK_ACCOUNT_TOKEN` (header `X-Postmark-Account-Token`); `POSTMARK_SERVER_TOKEN` (header `X-Postmark-Server-Token`) |
| [SendGrid](/reference/providers#sendgrid) | Event webhook or inbound-parse host per environment | recipe [`sendgrid.event_webhook`](/reference/recipes#sendgrid-event_webhook), recipe [`sendgrid.parse_setting`](/reference/recipes#sendgrid-parse_setting) | `SENDGRID_API_KEY` (bearer) |
| [Mailgun](/reference/providers#mailgun) | Webhook delivering email events to the preview URL, or an inbound route forwarding a per-preview address to it (sending and receiving domains are long-lived) | recipe [`mailgun.webhook`](/reference/recipes#mailgun-webhook), recipe [`mailgun.route`](/reference/recipes#mailgun-route) | `MAILGUN_API_KEY` (basic, user `api`) |

### Resend

Restricted API key or webhook per environment (domains are long-lived; test addresses like `delivered@resend.dev` need no API).

* **Ops**: recipe [`resend.api_key`](/reference/recipes#resend-api_key), recipe [`resend.webhook`](/reference/recipes#resend-webhook)
* **Credential**: `RESEND_API_KEY` (bearer)
* **Token**: [create it](https://resend.com/api-keys): An API key with Full access: Sending access can only send email, not manage webhooks, domains or keys. `sponson init --setup` checks it with `GET /domains`.
* **Detected by `sponson init` from**: `resend`, `RESEND_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: api_key
  adapter: http
  op: resource
  recipe: resend.api_key
  name: sponson-${ctx.env}-${ctx.scope}
```

### Postmark

Server per environment, webhook per server (chained in one plan by a line credential (ADR 0017, Line credentials): the webhook line says `destroy: keep`, since its webhooks go with the server; scenarios/adapters/http-line-credential.yaml).

* **Ops**: recipe [`postmark.server`](/reference/recipes#postmark-server), recipe [`postmark_server.webhook`](/reference/recipes#postmark_server-webhook)
* **Credential**: `POSTMARK_ACCOUNT_TOKEN` (header `X-Postmark-Account-Token`); `POSTMARK_SERVER_TOKEN` (header `X-Postmark-Server-Token`)
* **Token**: [create it](https://account.postmarkapp.com/api_tokens): The Account API token (Account Owner or Admin only); Postmark tokens have no scopes. `sponson init --setup` checks it with `GET /servers`.
* **Token**: [create it](https://postmarkapp.com/developer/api/overview): The server's Server API token (the server's API Tokens tab); Postmark tokens have no scopes. `sponson init --setup` checks it with `GET /server`.
* **Detected by `sponson init` from**: `postmark`, `POSTMARK_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: server
  adapter: http
  op: resource
  recipe: postmark.server
  name: sponson-${ctx.env}-${ctx.scope}
  inbound_hook_url: https://${ctx.scope}.preview.example.com/api/postmark/inbound
```

### SendGrid

Event webhook or inbound-parse host per environment.

* **Ops**: recipe [`sendgrid.event_webhook`](/reference/recipes#sendgrid-event_webhook), recipe [`sendgrid.parse_setting`](/reference/recipes#sendgrid-parse_setting)
* **Credential**: `SENDGRID_API_KEY` (bearer)
* **Token**: [create it](https://app.sendgrid.com/settings/api_keys): A Restricted Access key with Mail Settings > Event Notification set to Full Access. `sponson init --setup` checks it with `GET /v3/scopes`.
* **Detected by `sponson init` from**: `@sendgrid/*`, `SENDGRID_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: event_webhook
  adapter: http
  op: resource
  recipe: sendgrid.event_webhook
  url: https://${ctx.scope}.preview.example.com/api/sendgrid/events
  friendly_name: sponson ${ctx.env} ${ctx.scope}
```

### Mailgun

Webhook delivering email events to the preview URL, or an inbound route forwarding a per-preview address to it (sending and receiving domains are long-lived) (every account webhook receives every domain's events, and an event type takes three URLs at most: three previews at a time; per-domain webhooks (a list per event, or a map keyed by URL) are not used).

* **Ops**: recipe [`mailgun.webhook`](/reference/recipes#mailgun-webhook), recipe [`mailgun.route`](/reference/recipes#mailgun-route)
* **Credential**: `MAILGUN_API_KEY` (basic, user `api`)
* **Token**: [create it](https://documentation.mailgun.com/docs/mailgun/api-reference/authentication/): An account API key (Account Settings > API Keys): Domain Sending Keys 'only allow sending messages' and cannot manage webhooks or routes. `sponson init --setup` checks it with `GET /v1/webhooks`.
* **Detected by `sponson init` from**: `mailgun.js`, `mailgun-js`, `MAILGUN_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: webhook
  adapter: http
  op: resource
  recipe: mailgun.webhook
  name: sponson-${ctx.env}-${ctx.scope}
  url: https://pr-42.preview.example.com
```

## Observability per release

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Sentry](/reference/providers#sentry) | Release for the commit, deploy record for the environment; finalize | recipe [`sentry.release`](/reference/recipes#sentry-release), recipe [`sentry.deploy`](/reference/recipes#sentry-deploy) | `SENTRY_AUTH_TOKEN` (bearer) |
| [Datadog](/reference/providers#datadog) | Deployment marker / synthetic test against the preview URL | recipe [`datadog.synthetic_api_test`](/reference/recipes#datadog-synthetic_api_test) | `DD_API_KEY` (header `DD-API-KEY`) and `DD_APP_KEY` (header `DD-APPLICATION-KEY`) |
| [Honeycomb](/reference/providers#honeycomb) | Marker per deploy in the environment's dataset | recipe [`honeycomb.marker`](/reference/recipes#honeycomb-marker) | `HONEYCOMB_API_KEY` (header `X-Honeycomb-Team`) |
| [New Relic](/reference/providers#new-relic) | Deployment marker (change tracking) on the environment's entity per release | recipe [`newrelic.deployment`](/reference/recipes#newrelic-deployment) | `NEW_RELIC_API_KEY` (header `API-Key`) |
| [Rollbar](/reference/providers#rollbar) | Deploy of the revision in the environment (`preview`) per release | recipe [`rollbar.deploy`](/reference/recipes#rollbar-deploy) | `ROLLBAR_ACCESS_TOKEN` (header `X-Rollbar-Access-Token`) |
| [BugSnag](/reference/providers#bugsnag) | Build (release) of the app version in the environment's release stage per deploy | recipe [`bugsnag.build`](/reference/recipes#bugsnag-build) | none of Sponson's (calls a URL the line gives; a key it needs is a `{ secret }` param) |

### Sentry

Release for the commit, deploy record for the environment; finalize.

* **Ops**: recipe [`sentry.release`](/reference/recipes#sentry-release), recipe [`sentry.deploy`](/reference/recipes#sentry-deploy)
* **Credential**: `SENTRY_AUTH_TOKEN` (bearer)
* **Token**: [create it](https://docs.sentry.io/account/auth-tokens/): An organization auth token (Settings > Auth Tokens), or a personal token with project:releases. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `@sentry/*`, `sentry.client.config.ts`, `sentry.client.config.js`, `sentry.server.config.ts`, `sentry.server.config.js`, `sentry.properties`, `.sentryclirc`, `SENTRY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: release
  adapter: http
  op: resource
  recipe: sentry.release
  organization: acme
  version: app@${ctx.git.sha}
  project: web
  url: https://github.com/acme/app/commit/${ctx.git.sha}
```

### Datadog

Deployment marker / synthetic test against the preview URL (covered by a Synthetic API test against the preview URL; deployment markers stay out of reach (a later run could not find the event it made)).

* **Ops**: recipe [`datadog.synthetic_api_test`](/reference/recipes#datadog-synthetic_api_test)
* **Credential**: `DD_API_KEY` (header `DD-API-KEY`) and `DD_APP_KEY` (header `DD-APPLICATION-KEY`)
* **Token**: [create it](https://app.datadoghq.com/organization-settings/api-keys): An API key, plus an Application key (Organization Settings > Application Keys) limited to the scopes the lines need. `sponson init --setup` checks it with `GET /validate`.
* **Detected by `sponson init` from**: `dd-trace`, `@datadog/*`, `DD_*`, `DATADOG_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: synthetic_api_test
  adapter: http
  op: resource
  recipe: datadog.synthetic_api_test
  name: preview ${ctx.scope}
  url: https://${ctx.scope}.preview.example.app/api/health
  locations:
    - aws:us-east-2
```

### Honeycomb

Marker per deploy in the environment's dataset.

* **Ops**: recipe [`honeycomb.marker`](/reference/recipes#honeycomb-marker)
* **Credential**: `HONEYCOMB_API_KEY` (header `X-Honeycomb-Team`)
* **Token**: [create it](https://ui.honeycomb.io/): An environment Configuration Key (Manage Environments > API Keys) with only the permission the lines need, such as Manage Markers. `sponson init --setup` checks it with `GET /1/auth`.
* **Detected by `sponson init` from**: `@honeycombio/*`, `HONEYCOMB_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: marker
  adapter: http
  op: resource
  recipe: honeycomb.marker
  dataset: __all__
  message: deploy ${ctx.scope} ${ctx.git.sha}
  url: https://${ctx.scope}.preview.example.com
```

### New Relic

Deployment marker (change tracking) on the environment's entity per release (an `http.action` (ADR 0022) sent once per entity and version; markers are history (no undo); a preview needs its own entity, or it marks production's charts).

* **Ops**: recipe [`newrelic.deployment`](/reference/recipes#newrelic-deployment)
* **Credential**: `NEW_RELIC_API_KEY` (header `API-Key`)
* **Token**: [create it](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/): A user key (`NRAK-…`, API keys > Create a key > User) of a user who can create change tracking events for the entity's account. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `newrelic`, `@newrelic/*`, `NEW_RELIC_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: deployment
  adapter: http
  op: action
  recipe: newrelic.deployment
  entity_guid: MXxBUE18QVBQTElDQVRJT058MTIzNDU2Nzg
  version: ${ctx.git.sha}
  description: ${ctx.env} ${ctx.scope}
```

### Rollbar

Deploy of the revision in the environment (`preview`) per release (an `http.action` sent once per environment and revision; deploys are history (no delete in the API)).

* **Ops**: recipe [`rollbar.deploy`](/reference/recipes#rollbar-deploy)
* **Credential**: `ROLLBAR_ACCESS_TOKEN` (header `X-Rollbar-Access-Token`)
* **Token**: [create it](https://docs.rollbar.com/docs/access-tokens): A project access token with the `post_server_item` scope ('Can perform all POST requests to /deploy/ and /item/'). `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `rollbar`, `@rollbar/*`, `ROLLBAR_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: deploy
  adapter: http
  op: action
  recipe: rollbar.deploy
  environment: ${ctx.env}
  revision: ${ctx.git.sha}
  comment: ${ctx.scope}
```

### BugSnag

Build (release) of the app version in the environment's release stage per deploy (an `http.action` sent once per version and release stage; releases are history (no undo)).

* **Ops**: recipe [`bugsnag.build`](/reference/recipes#bugsnag-build)
* **Credential**: none of Sponson's (calls a URL the line gives; a key it needs is a `{ secret }` param)
* **Detected by `sponson init` from**: `@bugsnag/*`, `BUGSNAG_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: build
  adapter: http
  op: action
  recipe: bugsnag.build
  api_key:
    secret: env://BUGSNAG_API_KEY
  app_version: ${ctx.git.sha}
  release_stage: ${ctx.env}
```

## DNS and preview domains

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Cloudflare DNS](/reference/providers#cloudflare-dns) | CNAME `pr-42.preview.example.com` → the preview | recipe [`cloudflare.dns_cname`](/reference/recipes#cloudflare-dns_cname) | `CLOUDFLARE_API_TOKEN` (bearer) |
| [Vercel domains](/reference/providers#vercel-domains) | Branch-bound project domain (`gitBranch`) for a stable preview hostname | recipe [`vercel.project_domain`](/reference/recipes#vercel-project_domain) | `VERCEL_TOKEN` (bearer) |

### Cloudflare DNS

CNAME `pr-42.preview.example.com` → the preview.

* **Ops**: recipe [`cloudflare.dns_cname`](/reference/recipes#cloudflare-dns_cname)
* **Credential**: `CLOUDFLARE_API_TOKEN` (bearer)
* **Token**: [create it](https://dash.cloudflare.com/profile/api-tokens): A custom token (or the Edit zone DNS template) with Zone > DNS > Edit, its Zone Resources limited to the zone. `sponson init --setup` checks it with `GET /user/tokens/verify`.
* **Detected by `sponson init` from**: `wrangler`, `@cloudflare/next-on-pages`, `wrangler.json`, `wrangler.jsonc`, `wrangler.toml`, `CLOUDFLARE_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: dns_cname
  adapter: http
  op: resource
  recipe: cloudflare.dns_cname
  zone_id: 023e105f4ecef8ad9ca31a8372d0c353
  name: ${ctx.scope}.preview.example.com
  target: preview-host.example.net
  comment: Managed by Sponson
```

### Vercel domains

Branch-bound project domain (`gitBranch`) for a stable preview hostname.

* **Ops**: recipe [`vercel.project_domain`](/reference/recipes#vercel-project_domain)
* **Credential**: `VERCEL_TOKEN` (bearer)
* **Token**: [create it](https://vercel.com/account/tokens): A token scoped to the team that owns the project, with an expiration. `sponson init --setup` checks it with `GET /v2/user`.
* **Detected by `sponson init` from**: `vercel`, `@vercel/*`, `vercel.json`, `.vercel/project.json`, `VERCEL_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: project_domain
  adapter: http
  op: resource
  recipe: vercel.project_domain
  project: prj_12HKQaOmR5t5Uy6vdcQsNIiZgHGB
  team_id: team_1a2b3c4d5e6f7g8h9i0j1k2l
  name: ${ctx.scope}.preview.example.com
  git_branch: ${ctx.git.branch}
```

## CORS and allowed origins

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Firebase Storage (GCS bucket)](/reference/providers#firebase-storage-gcs-bucket) | Preview origin in the bucket's `cors` | recipe [`gcs.bucket_cors`](/reference/recipes#gcs-bucket_cors) | the token `gcloud auth print-access-token` prints (bearer) |
| [S3 / R2](/reference/providers#s3-/-r2) | Preview origin in the bucket CORS rules | recipe [`s3.bucket_cors`](/reference/recipes#s3-bucket_cors) | `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (AWS SigV4, `s3` in `us-east-1`) |
| [Sanity CORS](/reference/providers#sanity-cors) | Preview origin in the project's CORS origins, so browser code, an embedded Studio or the Presentation tool on the preview can query the Content Lake | recipe [`sanity.cors_origin`](/reference/recipes#sanity-cors_origin) | `SANITY_AUTH_TOKEN` (bearer) |

### Firebase Storage (GCS bucket)

Preview origin in the bucket's `cors`.

* **Ops**: recipe [`gcs.bucket_cors`](/reference/recipes#gcs-bucket_cors)
* **Credential**: the token `gcloud auth print-access-token` prints (bearer)
* **Token**: [set up access](https://cloud.google.com/storage/docs/using-cors): Sign in with `gcloud auth login` as an identity holding Storage Admin (roles/storage.admin) on the bucket: storage.buckets.get and storage.buckets.update. `sponson init --setup` shows this; there is no token to store.
* **Detected by `sponson init` from**: `@google-cloud/storage`

```yaml theme={null}
# under changes: (no provider block needed)
- id: bucket_cors
  adapter: http
  op: list_item
  recipe: gcs.bucket_cors
  bucket: my-app.appspot.com
  origin: https://${ctx.scope}.preview.example.app
```

### S3 / R2

Preview origin in the bucket CORS rules.

* **Ops**: recipe [`s3.bucket_cors`](/reference/recipes#s3-bucket_cors)
* **Credential**: `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (AWS SigV4, `s3` in `us-east-1`)
* **Token**: [create it](https://console.aws.amazon.com/iam/home#/users): AWS: an access key whose policy allows s3:GetBucketCORS and s3:PutBucketCORS on the bucket. R2: an API token with Admin Read & Write. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `@aws-sdk/client-s3`

```yaml theme={null}
providers:
  http:
    s3:
      recipe: s3
      base_url: https://assets.s3.us-east-1.amazonaws.com
changes:
  - id: bucket_cors
    adapter: http
    op: list_item
    recipe: s3.bucket_cors
    rule_id: sponson-${ctx.scope}
    origin: https://${ctx.scope}.preview.example.app
```

### Sanity CORS

Preview origin in the project's CORS origins, so browser code, an embedded Studio or the Presentation tool on the preview can query the Content Lake (previews that fetch only on the server need none; `allowCredentials` is set on create (no update is documented)).

* **Ops**: recipe [`sanity.cors_origin`](/reference/recipes#sanity-cors_origin)
* **Credential**: `SANITY_AUTH_TOKEN` (bearer)
* **Token**: [create it](https://www.sanity.io/manage): A robot token of the project (API → Tokens) with the Administrator role, or Developer on Growth and Enterprise: editors cannot create datasets, and CORS origins are project settings. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `sanity`, `@sanity/client`, `next-sanity`, `sanity.config.ts`, `sanity.config.js`, `sanity.cli.ts`, `sanity.cli.js`, `SANITY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: cors_origin
  adapter: http
  op: resource
  recipe: sanity.cors_origin
  project_id: abc123de
  origin: https://${ctx.scope}.preview.example.app
```

## Queues, caches, background jobs

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Upstash Redis](/reference/providers#upstash-redis) | Database per PR, REST URL and token out | recipe [`upstash.redis_database`](/reference/recipes#upstash-redis_database) | `UPSTASH_EMAIL` / `UPSTASH_API_KEY` (basic) |
| [Upstash QStash](/reference/providers#upstash-qstash) | Schedule that calls the preview URL | recipe [`qstash.schedule`](/reference/recipes#qstash-schedule) | `QSTASH_TOKEN` (bearer) |
| [Inngest](/reference/providers#inngest) | Sync the preview deployment into its branch environment | recipe [`inngest.sync`](/reference/recipes#inngest-sync) | none of Sponson's (calls a URL the line gives; a key it needs is a `{ secret }` param) |

### Upstash Redis

Database per PR, REST URL and token out.

* **Ops**: recipe [`upstash.redis_database`](/reference/recipes#upstash-redis_database)
* **Credential**: `UPSTASH_EMAIL` / `UPSTASH_API_KEY` (basic)
* **Token**: [create it](https://console.upstash.com/account/api): A Developer API key (Account > Developer API) with Read/Write; the account's email is its user name. `sponson init --setup` checks it with `GET /redis/databases`.
* **Detected by `sponson init` from**: `@upstash/redis`, `UPSTASH_REDIS_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: redis_database
  adapter: http
  op: resource
  recipe: upstash.redis_database
  name: app-${ctx.scope}
  primary_region: us-east-1
```

### Upstash QStash

Schedule that calls the preview URL.

* **Ops**: recipe [`qstash.schedule`](/reference/recipes#qstash-schedule)
* **Credential**: `QSTASH_TOKEN` (bearer)
* **Token**: [create it](https://console.upstash.com/qstash): The QStash token of the region (QStash tab); it has no scopes. `sponson init --setup` checks it with `GET /v2/schedules`.
* **Detected by `sponson init` from**: `@upstash/qstash`, `QSTASH_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: schedule
  adapter: http
  op: resource
  recipe: qstash.schedule
  schedule_id: cron-${ctx.scope}
  destination: https://${ctx.scope}.preview.example.com/api/cron
  cron: "*/15 * * * *"
```

### Inngest

Sync the preview deployment into its branch environment (an `http.action` (ADR 0022): `PUT` to the deployment's serve endpoint once per deployment URL and revision, no Inngest credential; the app must set `INNGEST_ENV` (automatic on Vercel); nothing to undo (branch environments archive themselves); the beta REST v2 sync endpoint is not used (no way to target a branch environment)).

* **Ops**: recipe [`inngest.sync`](/reference/recipes#inngest-sync)
* **Credential**: none of Sponson's (calls a URL the line gives; a key it needs is a `{ secret }` param)
* **Detected by `sponson init` from**: `inngest`, `INNGEST_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: sync
  adapter: http
  op: action
  recipe: inngest.sync
  url: https://acme-git-feat-x-acme.vercel.app
  revision: ${ctx.git.sha}
```

## Search, payments test mode

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Algolia](/reference/providers#algolia) | Index per environment, copied from a template index | [`algolia.index`](/reference/plan-format#algolia-index) | `ALGOLIA_API_KEY` |
| [Stripe test-mode objects](/reference/providers#stripe-test-mode-objects) | Products / prices / coupons the preview's code expects | recipe [`stripe.product`](/reference/recipes#stripe-product), recipe [`stripe.price`](/reference/recipes#stripe-price), recipe [`stripe.coupon`](/reference/recipes#stripe-coupon) | `STRIPE_SECRET_KEY` (bearer) |
| [Stripe sandboxes](/reference/providers#stripe-sandboxes) | A sandbox per PR | [`manual.step`](/reference/plan-format#manual-step) | none (a person does it) |

### Algolia

Index per environment, copied from a template index (copies records, settings, synonyms and rules (or a scope of them) and waits for the copy task; only the index's existence is compared for drift).

* **Ops**: [`algolia.index`](/reference/plan-format#algolia-index)
* **Credential**: `ALGOLIA_API_KEY`
* **Token**: [create it](https://dashboard.algolia.com/account/api-keys/): A restricted API key (not the Admin key) with the addObject, deleteIndex, editSettings, settings and listIndexes ACLs. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `algoliasearch`, `@algolia/*`, `instantsearch.js`, `react-instantsearch`, `ALGOLIA_*`

```yaml theme={null}
# from scenarios/adapters/algolia.yaml
providers:
  algolia: { application_id: APP123 }
changes:
  - id: search
    adapter: algolia
    op: index
    name: products_${ctx.scope}
    copy_from: products
    environments: [preview]
```

### Stripe test-mode objects

Products / prices / coupons the preview's code expects.

* **Ops**: recipe [`stripe.product`](/reference/recipes#stripe-product), recipe [`stripe.price`](/reference/recipes#stripe-price), recipe [`stripe.coupon`](/reference/recipes#stripe-coupon)
* **Credential**: `STRIPE_SECRET_KEY` (bearer)
* **Token**: [create it](https://dashboard.stripe.com/test/apikeys): A test-mode restricted key (Create restricted key) with Webhook Endpoints set to Write and everything else None. `sponson init --setup` checks it with `GET /webhook_endpoints`.
* **Detected by `sponson init` from**: `stripe`, `@stripe/*`, `STRIPE_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: product
  adapter: http
  op: resource
  recipe: stripe.product
  product_id: pro-${ctx.scope}
  name: Pro (preview)
```

### Stripe sandboxes

A sandbox per PR.

* **Ops**: [`manual.step`](/reference/plan-format#manual-step)
* **Credential**: none (a person does it)
* **Detected by `sponson init` from**: not detected (write the line yourself)

```yaml theme={null}
# from examples/google-oauth-manual-step.plan.yaml
changes:
  - 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]
```

## Headless CMS content per preview

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Contentful](/reference/providers#contentful) | Sandbox environment per PR copied from master (entries, assets, content types, locales), readable by the app's delivery and preview API keys; deleted when the PR closes | [`contentful.environment`](/reference/plan-format#contentful-environment), [`contentful.api_key_access`](/reference/plan-format#contentful-api_key_access) | `CONTENTFUL_MANAGEMENT_TOKEN` |
| [Sanity](/reference/providers#sanity) | Dataset per PR for the preview's content (empty, imported or seeded by the app; or a Cloud Clone copy of production on Enterprise) | recipe [`sanity.dataset`](/reference/recipes#sanity-dataset) | `SANITY_AUTH_TOKEN` (bearer) |

### Contentful

Sandbox environment per PR copied from master (entries, assets, content types, locales), readable by the app's delivery and preview API keys; deleted when the PR closes (waits until the copy is ready; the key's other environments are left alone; a space holds at most 151 environments and plans include fewer sandboxes (2 to 4 below Enterprise)).

* **Ops**: [`contentful.environment`](/reference/plan-format#contentful-environment), [`contentful.api_key_access`](/reference/plan-format#contentful-api_key_access)
* **Credential**: `CONTENTFUL_MANAGEMENT_TOKEN`
* **Token**: [create it](https://www.contentful.com/developers/docs/references/authentication/): A personal access token (or an OAuth token) with the content\_management\_manage scope, of a user whose role may create and delete sandbox environments and edit API keys in the space. `sponson init --setup` checks it with one read-only request.
* **Detected by `sponson init` from**: `contentful`, `contentful-management`, `@contentful/*`, `CONTENTFUL_*`

```yaml theme={null}
# from scenarios/adapters/contentful.yaml
providers:
  contentful: { space: space1 }
changes:
  - id: content
    adapter: contentful
    op: environment
    name: ${ctx.scope}
    environments: [preview]
```

### Sanity

Dataset per PR for the preview's content (empty, imported or seeded by the app; or a Cloud Clone copy of production on Enterprise) (an empty dataset per scope; Cloud Clone copies are not covered. Free and Growth include 2 datasets in all, so one preview dataset at a time below Enterprise).

* **Ops**: recipe [`sanity.dataset`](/reference/recipes#sanity-dataset)
* **Credential**: `SANITY_AUTH_TOKEN` (bearer)
* **Token**: [create it](https://www.sanity.io/manage): A robot token of the project (API → Tokens) with the Administrator role, or Developer on Growth and Enterprise: editors cannot create datasets, and CORS origins are project settings. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `sanity`, `@sanity/client`, `next-sanity`, `sanity.config.ts`, `sanity.config.js`, `sanity.cli.ts`, `sanity.cli.js`, `SANITY_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: dataset
  adapter: http
  op: resource
  recipe: sanity.dataset
  project_id: abc123de
  name: preview-${ctx.scope}
```

## Backend platforms with preview deployments

| Provider | What Sponson manages | Ops | Credential |
| - | - | - | - |
| [Convex](/reference/providers#convex) | Preview deployment per PR (its own functions, data and URL), its deploy key for the preview's build; deleted when the PR closes instead of left to expire | recipe [`convex.preview_deployment`](/reference/recipes#convex-preview_deployment), recipe [`convex.deploy_key`](/reference/recipes#convex-deploy_key) | `CONVEX_TEAM_ACCESS_TOKEN` (bearer) |

### Convex

Preview deployment per PR (its own functions, data and URL), its deploy key for the preview's build; deleted when the PR closes instead of left to expire (the deployment and a deploy key for it (a once-only output, for `CONVEX_DEPLOY_KEY`); pushing code stays `npx convex deploy`).

* **Ops**: recipe [`convex.preview_deployment`](/reference/recipes#convex-preview_deployment), recipe [`convex.deploy_key`](/reference/recipes#convex-deploy_key)
* **Credential**: `CONVEX_TEAM_ACCESS_TOKEN` (bearer)
* **Token**: [create it](https://dashboard.convex.dev/team/settings/access-tokens): A team access token (Team Settings → Access Tokens), preferably of a service-account member: it can do what that member's role allows on the team. `sponson init --setup` cannot check it (no read-only request every such credential may send).
* **Detected by `sponson init` from**: `convex`, `convex.json`, `CONVEX_*`

```yaml theme={null}
# under changes: (no provider block needed)
- id: preview_deployment
  adapter: http
  op: resource
  recipe: convex.preview_deployment
  project_id: "12345"
  reference: ${ctx.scope}
```


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