Skip to main content
How much of the per-environment SaaS state a typical US/EU web team touches Sponson can manage today, how that was measured, and what it takes to cover the rest. Written against Sponson 0.4.0 on 2026-10-11; the built-in adapters it counts are the ones listed in docs/plan-format.md. API facts come from the providers’ published docs (Sources); a fact marked (unverified) comes from memory or a secondary source and needs checking before an adapter relies on it.

Scope: what ships beside the code

A piece of SaaS state is in scope when all four hold: Out of scope, by design (Terraform’s or Pulumi’s job, see ROADMAP.md): the parent objects these hang off (projects, tenants, applications, accounts, zones, buckets, clusters, flag definitions), organisation, IAM and billing, networking, and anything that outlives every environment. Also out of scope: build artifacts (source-map uploads), application data (seed rows belong to the app’s migration tool) and the deploy itself beyond triggering and waiting on it (vercel.deploy). Production lines are in scope only for the state a release changes; long-lived production objects enter a plan as { keep: true } adoptions, never as creations.

Coverage matrix

Columns:
  • Side effect: what one environment adds, changes or removes.
  • API shape: endpoint or CLI, auth, and the write model: object (one API object per item, created and deleted on its own) or list (one array or map on a parent object, read-modify-write). For list shapes, concurrent writers would lose updates; the op declares the parent object (lockOn) and the engine serialises Sponson’s writers with a lock per object (ADR 0019), on top of any precondition the provider offers.
  • Generic?: could the declarative HTTP adapter described under Strategy express it? Y yes, P partly (the lifecycle needs more than request templates, noted), N no.
  • Pri: how often it appears in preview-environment workflows. P0 most preview setups on that stack hit it; P1 common; P2 occasional. An estimate from public templates, integrations and docs, not a survey.
  • Now: yes when a built-in op or a shipped recipe covers it; manual step when no API can and a manual.step line tracks what a person does (ADR 0021), never counted as automated; no (no API) when nothing can.
The rows live in coverage.yaml (covered_by names the op or recipe op that manages each one) and the tables below are generated from it: edit the data, then run pnpm docs:gen.

Deploy targets and their environment variables

Databases with branching or per-PR instances

Auth callback and redirect allow-lists

Feature flags and targeting

Webhooks registered per environment

Email: sending domains, webhooks, test inboxes

Observability per release

DNS and preview domains

CORS and allowed origins

Queues, caches, background jobs

Search, payments test mode

Headless CMS content per preview

Backend platforms with preview deployments

Considered and not added

Checked on 2026-10-12 against the providers’ docs, and left out of the matrix because one of the four scope tests fails. A row is added when that changes; none of these is counted, covered or not.

Secret managers (read side: secret sources, not side effects)

Secret sources resolve { secret } references; they create nothing, so they are counted separately. The built-in ones are listed in docs/plan-format.md. The write side (a per-PR Doppler branch config, POST /v3/configs) is a side effect; it is P2 and not counted above.

Current coverage

Every number below is computed from coverage.yaml by pnpm docs:gen; a row counts as covered when a registered op or a shipped recipe manages it. Counted over the 67 side-effect rows of the matrix (rows marked n/a have nothing to manage). Weights: P0 = 3, P1 = 2, P2 = 1 (9 P0, 18 P1, 40 P2; total weight 103). Covered: Vercel (vercel.env, vercel.deploy); Netlify (netlify.env); Cloudflare Pages / Workers (cloudflare.pages_env); Render (recipe render.env_var); Railway (recipe railway.variable); Fly.io (recipe fly_machines.app, recipe fly.secret); Heroku (recipe heroku.config_var); AWS Amplify (recipe amplify.branch, recipe amplify.branch_variable); Google Cloud Run (cloud_run.revision); Azure Static Web Apps (recipe azure_swa.build_app_setting); DigitalOcean App Platform (digitalocean.app); Firebase Hosting (recipe firebase_hosting.channel); Neon (neon.branch); Supabase (supabase.branch); PlanetScale (planetscale.branch, planetscale.password); Turso (recipe turso.database_branch); Xata (xata.branch); MongoDB Atlas (recipe mongodb_atlas.database_user); CockroachDB Cloud (recipe cockroachdb.database); Prisma Postgres (recipe prisma.database, recipe prisma.database_copy); Clerk (clerk.redirect_allow); Auth0 (recipe auth0.callback_url, recipe auth0.logout_url, recipe auth0.web_origin, recipe auth0.allowed_origin); Supabase Auth (supabase.auth_redirect); Firebase Auth (recipe firebase.authorized_domain); WorkOS (recipe workos.redirect_uri); Stytch (recipe stytch.redirect_url); Okta (recipe okta.redirect_uri, recipe okta.post_logout_redirect_uri, recipe okta.trusted_origin); Kinde (recipe kinde.callback_url, recipe kinde.logout_url); LaunchDarkly (launchdarkly.flag_target); PostHog (recipe posthog.release_condition); Statsig (recipe statsig.gate_rule); GrowthBook (recipe growthbook.force_rule); Unleash (recipe unleash.flag_strategy); Flagsmith (recipe flagsmith.environment); ConfigCat (recipe configcat.targeting_rule); Split (Harness FME) (recipe split.targeting_rule); Stripe (recipe stripe.webhook_endpoint); GitHub (recipe github.repo_webhook); Svix (as a sender) (recipe svix.endpoint); Twilio (recipe twilio.twiml_app); Ably (recipe ably.webhook_rule); Resend (recipe resend.api_key, recipe resend.webhook); Postmark (recipe postmark.server, recipe postmark_server.webhook); SendGrid (recipe sendgrid.event_webhook, recipe sendgrid.parse_setting); Mailgun (recipe mailgun.webhook, recipe mailgun.route); Sentry (recipe sentry.release, recipe sentry.deploy); Datadog (recipe datadog.synthetic_api_test); Honeycomb (recipe honeycomb.marker); New Relic (recipe newrelic.deployment); Rollbar (recipe rollbar.deploy); BugSnag (recipe bugsnag.build); Cloudflare DNS (recipe cloudflare.dns_cname); Vercel domains (recipe vercel.project_domain); Firebase Storage (GCS bucket) (recipe gcs.bucket_cors); S3 / R2 (recipe s3.bucket_cors); Sanity CORS (recipe sanity.cors_origin); Upstash Redis (recipe upstash.redis_database); Upstash QStash (recipe qstash.schedule); Inngest (recipe inngest.sync); Algolia (algolia.index); Stripe test-mode objects (recipe stripe.product, recipe stripe.price, recipe stripe.coupon); Contentful (contentful.environment, contentful.api_key_access); Sanity (recipe sanity.dataset); Convex (recipe convex.preview_deployment, recipe convex.deploy_key). Automated coverage counts only rows an op manages without a person. Rows no API reaches (manual, or a manual.step line that records the step a person does; weight 5): Google OAuth clients (no public API for standard OAuth web clients (re-checked 2026-10-12): Google’s web-server guide configures redirect URIs on the Console Clients page only (developers.google.com/identity/protocols/oauth2/web-server), the IAM oauthClients API and gcloud iam oauth-clients are ‘only for Workforce Identity Federation’ (docs.cloud.google.com/iam/docs/workforce-manage-oauth-app, docs.cloud.google.com/iam/docs/reference/rest/v1/projects.locations.oauthClients), and the IAP OAuth Admin API ‘was shut down on March 19, 2026’ and never allowed updating redirect URIs (docs.cloud.google.com/iap/docs/programmatic-oauth-clients)); Clerk webhooks (Clerk’s API manages the Svix app, not its endpoints (re-checked 2026-10-12): the Backend API spec (github.com/clerk/openapi-specs, bapi/2025-11-10.yml and 2026-05-12.yml) has only CreateSvixApp, DeleteSvixApp and GenerateSvixAuthURL (‘Generate a new URL for accessing the Svix’s management dashboard’), the private-beta Platform API (platform/beta.yml) has no webhook routes, the clerk api ls webhook route the webhooks overview points agents to (clerk.com/docs/guides/development/webhooks/overview) lists those same three Backend API routes, clerk webhooks is a local relay and verifier that makes ‘No Clerk API calls’ (github.com/clerk/cli, commands/webhooks), and Clerk documents no Svix API token for its app (a portal-URL token is undocumented)); Stripe sandboxes (no public API for sandboxes in your own account (re-checked 2026-10-12): docs.stripe.com/sandboxes/dashboard/manage creates and deletes them in the Dashboard only, up to five per account, the Claimable Sandboxes API (docs.stripe.com/sandboxes/claimable-sandboxes) makes anonymous sandboxes that no existing account owns, for coding platforms that set up Stripe for their users, needs a Stripe App that Stripe reviews, uses a preview API version and has no delete (unclaimed ones are deleted after 60 days), and stripe sandbox create (docs.stripe.com/cli/sandbox) provisions an account-less sandbox only when the CLI is logged out (‘If you’re already logged in, sandbox create opens the sandbox management page in your Stripe Dashboard’) that expires after 7 days). They cap coverage at 64 rows, 98 of 103 weighted (95.1%). Recipe candidates, rows the generic adapter can express fully (Y) that nothing covers yet: 0 ().

Strategy

Two kinds of adapter close the gap. First-class adapters (TypeScript, one sim routes file, scenarios) for rows whose lifecycle is more than create/read/delete: asynchronous provisioning (branches, Supabase previews), an external event barrier (Netlify and Railway deploys, like vercel.env), outputs that exist only once behind a multi-step lifecycle (PlanetScale password; a once-only output of a plain object, such as Stripe’s webhook secret, is a recipe), instruction-based diffs (LaunchDarkly), GraphQL, or non-bearer signing (SigV4). The generic HTTP adapter and its recipes (adapter: http, ADR 0017 and ADR 0020) for the long tail where each item is a URL, a rule or a record and the lifecycle is plain CRUD. A row counts as covered once a recipe for it ships: a verified, tested, documented spec a plan line names in one line, not a request template every team writes. The rows marked Y that no recipe covers yet are listed under Current coverage.

What adapter: http must support to cover the P0/P1 rows

  1. Provider block with credentials by reference: base URL, auth as bearer, custom header(s) (STATSIG-API-KEY, Algolia’s two headers), Basic, a token from an OAuth2 client-credentials exchange (Auth0, Kinde, Okta), or a token from a command (gcloud auth print-access-token for Firebase/GCS), SigV4 or Digest. Credentials are environment variable names, never values. Done (ADR 0017 and its amendment), with XML bodies and GraphQL requests.
  2. Two write models: object (create, read by id, update, delete) and list-on-parent (read the parent, find the item in an array, write the parent back). List mode must handle an array at a JSON path, a delimited string (Supabase uri_allow_list), and a keyed map with merge-patch delete-by-null (Cloudflare Pages env_vars, Heroku config vars).
  3. Request templates for read, create, update and delete: method, path, query (updateMask, site_id), body, with ${ctx.*}, params, { from } and { secret } substitution; body encodings JSON, form-encoded (Stripe), JSON Patch, merge-patch, and a custom Content-Type (LaunchDarkly semantic patch).
  4. Response mapping by JSON path: the id, the fields compared for drift (hashed in the same canonical form as ResourceRecord.hash), and named outputs, each markable sensitive.
  5. Identity and lost-response recovery: a resource key template, and a find request (list + match predicate on a field such as url or name) so a create whose response was lost is found and claimed instead of duplicated; Idempotency-Key from the ledger key where the provider honours it (Stripe), client-chosen ids where it takes them (Svix uid, QStash Upstash-Schedule-Id).
  6. Preconditions and serialization for list mode: send the read version back when the provider has one (ETag / If-Match, GCS ifMetagenerationMatch), re-read after write to confirm the item landed, and declare the parent object (lockOn) so the engine’s per-object lock serialises Sponson’s writers (ADR 0019). Without this, two PRs editing one Auth0 application lose writes.
  7. Status classification overrides: which statuses mean “already exists” (re-read and claim), “gone” (success on delete), “busy” (retry), on top of packages/adapters/src/http.ts’s defaults; plus pagination styles (cursor, next link, page number).
  8. Destroy modes: delete, update to an archived state (Stripe prices active=false), remove-from-list, or leave (Sentry releases are history). delete and leave exist (destroy: keep); a recipe op sets the default (destroy: keep, or never when the provider cannot delete; ADR 0020, amendment).
  9. Simple polling: after create, poll a URL until a JSON path equals a value, with a timeout (Algolia tasks, Turso or Prisma readiness). Long multi-step lifecycles stay first-class.
  10. Scope listing for drift and sponson init: a list request plus a filter (name prefix, URL host) so unmanaged items are reported and adoptable.
  11. Once-only outputs: an output that is only in the create response must be declared as such, so the engine feeds it to dependent lines in the same run and refuses a later run that needs it (instead of reading an empty value). Needed for Stripe webhook secrets, API keys and DB passwords even in first-class adapters. The engine side exists: OutputSpec.once (ADR 0018), first used by planetscale.password; http.resource declares one as { path, sensitive: true, once: true } (ADR 0020, amendment; stripe.webhook_endpoint). Archiving destroys (item 8) are delete: { method: POST, body: … } (stripe.price).
  12. Sim support: a generic sim route set driven by the same declaration, so every http line gets the chaos and drift scenarios the built-in adapters get.

The next eight first-class adapters

Ranked by weighted rows covered, then by how much they need more than adapter: http. With these eight plus recipes for the Y rows, the remaining rows are Fly.io, Amplify, Cloud Run, Azure Static Web Apps, DigitalOcean, Xata, Algolia (all P1/P2 with partial generic fit or SigV4 needs; MongoDB Atlas and CockroachDB turned out to be recipes) and the three rows with no API.

Sources

The Netlify, Cloudflare Pages, Fly.io, Supabase Auth, Turso, Prisma Postgres, Clerk webhook, Firebase, WorkOS, Stytch, Kinde, Google OAuth, Statsig and Stripe sandbox facts were re-checked against these pages on 2026-10-11. The rest come from the providers’ docs as the author knew them and were not re-fetched for this document; treat them like (unverified) until an adapter’s verification table (docs/api-verification.md) covers them.