supabase provider for stackql
This repository builds and documents the supabase provider for StackQL, enabling SQL-based query and provisioning operations against the Supabase Management API - organizations and members, projects, preview branches, edge functions, secrets and API keys, project configuration (auth, Postgres, pooler, API, storage, realtime, SSL enforcement), custom domains, network restrictions and bans, backups and restore points, read replicas, add-ons, advisors, analytics, and the project SQL query endpoint.
The provider maps the Management API at https://api.supabase.com (the control plane). The per-project data APIs (PostgREST at <ref>.supabase.co/rest/v1, Realtime, Storage object I/O, GoTrue user-facing auth) are per-project hosts with per-project keys - a different surface, reserved as a possible future supabase_project sibling provider and out of scope here.
Supabase's official Terraform provider is labelled Public Alpha by the vendor and covers seven resources. This provider's coverage is generated mechanically from the vendor's published OpenAPI document - 158 operations across 14 services and 65 resources - and includes one capability outside the resource model: the project SQL query endpoint surfaced in-session, so the control plane (projects, config, secrets) and the project database itself are queryable in one place.
- Fixed server, PAT bearer auth - the API base is the literal
https://api.supabase.com. Authentication isAuthorization: Bearerwith a personal access token in theSUPABASE_ACCESS_TOKENenvironment variable (the Supabase CLI's and the Terraform provider's convention). - Project scope is a server variable - 144 of the 170 operations address a project by
ref; all but the project get/update/delete (which keep their root path) are generated on the server templatehttps://api.supabase.com/v1/projects/{ref}withrefresolved fromSUPABASE_PROJECT_ID(x-stackQL-envVar) when it is set, so single-project queries need noWHERE refclause. AWHERE ref = '...'value always takes precedence. - Spec fetch is pinned - the Management API serves its own unversioned OpenAPI document at
https://api.supabase.com/api/v1-json.bin/fetch-spec.shdownloads, fixes deterministically, validates and pins it (URL, date, sha256) inprovider-dev/config/spec_pin.json; the snapshot is committed so every refresh is a reviewed diff. - Rate limit as a design input - the Management API allows a fixed number of requests per minute per user (documented as 120; lower on analytics and database context endpoints). Test harnesses run serially and pace at 1.2 s; a 429 in CI is a harness bug, not a retry case.
- Config-as-rows is the audit surface - auth settings (237 flat columns), Postgres settings, SSL enforcement and network restrictions per project are the posture queries the docs lead with.
- The query endpoint is the flagship -
INSERT INTO supabase.database.queries (ref, query) SELECT ... RETURNING rowsruns SQL against the project database and returns the result set as one row'srowscolumn. - Deterministic pipeline - every step is scripted and re-runnable; mapping decisions are rules in scripts, never hand-edits to derived artifacts.
provider-dev/config/all_services.csvis committed as the durable record of every operation -> resource.method mapping, so a regeneration that moves or renames a method is a reviewable diff. - Update semantics labelled honestly - the API mixes PATCH and PUT; both map as
UPDATEuntil full-replacement semantics are proven per resource.
Cross-build findings from the sibling providers (clickhouse, hetzner, snowflake, keycloak, newrelic NOTES.md) are reused, not re-derived. Supabase-specific findings - the query endpoint decision, the spec fix classes, the project-scope server variable, the engine's string-typed UPDATE values, the bare-array secrets bodies - are recorded in NOTES.md.
- Node.js >= 20
- A local
stackqlbinary for testing ($STACKQL,./stackql, or onPATH;bin/start-server.shdownloads one only if none is found) - GNU make and bash (Linux, WSL or macOS); Python 3 and yarn for the smoke suite and the website
- For live smoke tests: a Supabase account, a personal access token, and a standing free-tier dev project (never a production organization or project)
Install dependencies:
npm installEvery step below is wrapped as a make target (make help lists them). The composite targets:
make all # deps, full pipeline (fetch/pin verify, inventory, split, mappings, pre-normalize,
# normalize, generate, post-process), offline + integration + meta-route tests,
# docs generation, website build - no credentials needed
make test # the three credential-free test layers
make smoke # live smoke suite against the standing dev project (sources .env if present)make all never touches a real account - the live suites are separate targets (smoke, smoke-live against the published provider, smoke-read-only, smoke-project-lifecycle for the gated project create/pause/delete, smoke-cleanup to sweep breadcrumbs). Live credentials are read from the environment or a gitignored .env file (see .env.example):
SUPABASE_ACCESS_TOKEN=sbp_... # personal access token
SUPABASE_PROJECT_ID=abcdefghijklmnopqrst # the standing dev project's refmake fetch-spec # verify against the recorded pin (fails on drift)
make refresh-spec # accept an upstream change (rewrites the pin - review the diff)Pinned snapshot (2026-08-27): Supabase API (v1), OpenAPI 3.0.0, 115 paths, 170 operations, upstream sha256 660e5634fab8.... Six deterministic fix classes are applied before validation and counted in the pin: "type": "null" -> nullable: true (5), propertyNames removed (3), numeric exclusiveMinimum lowered to the 3.0 form (3), $schema dialect keys removed (2), const -> enum (2), and hideDefinitions (0 in this snapshot). The 2026-08 refresh added one operation (a Prometheus metrics scrape, skipped as text) and changed 86 schemas.
make inventoryWrites provider-dev/config/endpoint_inventory.csv: one row per operation with ref/slug scoping, pagination parameters, request body kind (multipart/eszip/bare-array flagged), update-verb semantics presumption, the vendor's [Beta]/[Alpha] label, the deprecated flag, response shape, proposed service/resource/verb, and a skip reason where an operation is not mapped.
Inventory of the pinned snapshot: 170 operations, 158 mapped and 12 skipped with reason codes (4 oauth_user_agent_flow, 3 non_json_text_response, 1 multipart_eszip_deploy, 1 untyped_function_body, 1 untyped_json_response, 1 bare_array_bulk_body, 1 head_count_endpoint). 35 operations carry [Beta], 1 [Alpha], 5 are deprecated. 144 operations scope by project ref, 6 by organization slug, 8 by branch id, 12 by the token itself.
The service split is recorded as ordered path rules in provider-dev/config/service_names.json (first match wins, unmatched paths fail the build; oauth is classified but excluded from the provider since every operation in it is skip-coded):
| Service | Resources |
|---|---|
projects |
projects, organization_projects, available_regions, service_health, restore_versions, upgrade_eligibility, upgrade_status, read_replicas, claim_tokens, disk_configs, disk_autoscale_configs, disk_utilization |
organizations |
organizations, members, entitlements, project_claims |
branches |
branches, branch_configs, action_runs |
config |
auth_configs, auth_signing_keys, legacy_signing_keys, sso_providers, third_party_auth_integrations, postgres_configs, pooler_configs, pgbouncer_configs, postgrest_configs, storage_configs, realtime_configs, ssl_enforcement_configs, pgsodium_configs |
network |
network_restrictions, network_bans |
domains |
custom_hostnames, vanity_subdomains |
functions |
edge_functions |
secrets |
secrets, api_keys, legacy_api_keys |
database |
queries, migrations, backups, backup_schedules, restore_points, snippets, databases, jit_access, jit_role_mappings, jit_invites, jit_access_configs, readonly_mode, typescript_types, webhooks, cli_login_roles |
storage |
buckets |
billing |
addons |
analytics |
logs, all_logs, api_counts, api_request_counts, function_stats |
advisors |
security_lints, performance_lints |
profile |
profiles |
make splitbin/split.mjs splits by the path rules, then rebases every project-scoped path onto the server template in provider-dev/config/servers.json (dropping the /v1/projects/{ref} prefix and the ref path parameter, which becomes the server variable). The 18 non-project paths keep their full path and are pinned back to the API base by the post-process step.
make mappingsRegenerates provider-dev/config/all_services.csv from scratch and populates the stackql_* columns deterministically (map_operations.mjs: resource derivation from the path, RESOURCE_RULES and METHOD_RULES for the named exceptions). Validates before writing: every operation mapped or skipped with a reason, every spec operation present in the CSV, (resource, method) unique per service, and unique required-parameter signatures per (resource, sqlVerb).
| Operation pattern | StackQL verb | Resource / method |
|---|---|---|
| GET collection | SELECT |
<resource>.list (bare arrays wrapped by normalize; envelopes carry their key) |
| GET single / config singleton | SELECT |
<resource>.get |
| POST create | INSERT |
<resource>.create |
| PATCH / PUT edit | UPDATE |
<resource>.update |
| DELETE | DELETE |
<resource>.delete |
POST .../database/query |
INSERT (RETURNING rows) |
database.queries.run; the read-only sibling is EXEC queries.run_read_only |
| lifecycle actions (pause, restart, restore, upgrade, branch push/merge/reset, hostname activate, ...) | EXEC |
<resource>.<action> |
| POST-backed reads (network bans) | SELECT |
network_bans.list |
Mapped: 71 SELECT, 17 INSERT, 21 UPDATE, 17 DELETE, 32 EXEC.
make pre-normalize normalizepre_normalize.mjs applies the Supabase-specific adjustments (the eszip request variant and the deprecated query-parameter duplicates on the edge function create/update, the query endpoint's result schema, the secrets bulk bodies rewritten to single-item objects, the pooler config's camelCase duplicate column); the provider-utils normalize pass then flattens allOf, lowers oneOf/anyOf, and wraps the 13 bare-array list responses.
make generatewhich runs:
rm -rf provider-dev/openapi/*
npm run generate-provider -- \
--provider-name supabase \
--input-dir provider-dev/source \
--output-dir provider-dev/openapi/src/supabase \
--config-path provider-dev/config/all_services.csv \
--servers provider-dev/config/servers.json \
--provider-config '{"auth": {"type": "bearer", "credentialsenvvar": "SUPABASE_ACCESS_TOKEN"}, "snake_case_aliases": true}' \
--naive-req-body-translate \
--overwrite
node provider-dev/scripts/post_process.mjs--naive-req-body-translate exposes top-level request body properties as columns, so INSERT INTO supabase.secrets.secrets (name, value) ... and UPDATE supabase.config.auth_configs SET disable_signup = 'true' render the wire bodies as written. post_process.mjs pins the non-project paths to the API base (path-level servers), configures cursor pagination on snippets.list, sets request.nativeCasing: camel on the three camelCase-body methods, attaches the query endpoint's result binding, the POST-backed network_bans.list objectKey, the secrets request transforms (single-item object -> the bare array the wire expects) and naive body translation on the two DELETEs with bodies, and validates that every other path is project-relative.
The only server variable is ref. With SUPABASE_PROJECT_ID exported it is resolved automatically:
SELECT disable_signup, mfa_totp_enroll_enabled FROM supabase.config.auth_configs;A WHERE ref = '...' value takes precedence (one session, several projects); with the variable unset the parameter is required and listed by SHOW METHODS. projects.list, the organization surface, profile and snippets need neither. A JOIN cannot fan out over projects on ref (the config rows do not echo it); the estate posture pattern is a projects list followed by per-ref reads composed with UNION ALL.
Provider config: {"auth": {"type": "bearer", "credentialsenvvar": "SUPABASE_ACCESS_TOKEN"}}. A different variable can be passed at runtime with --auth='{"supabase": {"type": "bearer", "credentialsenvvar": "..."}}'.
INSERT and EXEC send typed JSON (booleans, numbers, JSON-shaped strings parsed into arrays). The stackql engine marshals every UPDATE ... SET value as a string (SET disable_signup = 'true', password_min_length = '12'); whether the Management API coerces string-typed booleans and numbers is established by the live smoke suite's auth-config toggle (NOTES.md finding 14).
Four layers, in order. Every regeneration is followed by the first three before commit (make test); the fourth is live.
make test-offline # node tests/offline_validation.mjsSHOW SERVICES / SHOW RESOURCES / SHOW METHODS and DESCRIBE EXTENDED against the local file registry - 38 checks: the 14 services and 65 resources, the verb mapping on projects.projects, that ref is required only when SUPABASE_PROJECT_ID is unset, the queries.run INSERT binding, the wide flat auth config (> 200 columns), the snake_case aliases on SSL enforcement, the pooler duplicate dropped, the single-item secrets bodies, the naive DELETE body on network_bans, the branch method split.
make test-integration # add -- --verbose for per-query outputRuns the provider against an in-process mock of the Management API (tests/integration/mock_supabase_server.mjs) serving the wire shapes the spec declares and enforcing the bearer token. The runner materialises a test copy of the registry with the server URLs pointed at the mock (server variables and the x-stackQL-envVar extension preserved) and asserts 66 row-level checks: bare-array wraps and single reads, the bearer header, SUPABASE_PROJECT_ID resolution vs a WHERE ref override vs the unset failure mode, the root paths on their overrides, the secrets bulk INSERT/DELETE wire bodies, an auth-config UPDATE toggle and restore, the snake_case corners (requested_config -> requestedConfig, file_size_limit -> fileSizeLimit, db_allowed_cidrs -> dbAllowedCidrs), the POST-backed bans read and the DELETE with a body, the query endpoint (INSERT ... RETURNING rows flowing a 2-row fixture, the read_only flag, the read-only sibling), an EXEC lifecycle action, an edge function lifecycle, API key pushdown and lifecycle, snippets cursor pagination across two pages, the envelope object keys, and the 404. tests/integration/probe.mjs runs ad-hoc statements against the mock and prints the wire calls.
make test-meta # npm run start-server / test-meta-routes -- supabase / stop-serverWalks every service, resource and method over a local wire server: 14 services, 65 resources, 158 methods, 71 selectable, no failures.
make smoke # reads + cheap write lifecycles + the query round trip (local registry)
make smoke-live # the same against the published provider (post-publish verification)
make smoke-read-only # read smokes only
make smoke-project-lifecycle # additionally the gated project create / pause / delete (minutes, free-tier quota)
make smoke-cleanup # sweep stackql-smoke-* breadcrumbstests/smoke_test.py (pystackql) runs against the standing free-tier dev project named by SUPABASE_PROJECT_ID: read smokes (profile, organizations, the project estate, the posture set, secrets, API keys, edge functions, branches, health, add-ons, security lints, backups, storage, migrations, snippets) and self-cleaning write lifecycles - a secret INSERT/SELECT/DELETE, an API key INSERT/SELECT/UPDATE/DELETE, an edge function INSERT/UPDATE/DELETE (the vendor-deprecated JSON create), an auth-config toggle-and-restore (the string-typed UPDATE probe), an idempotent network-restrictions re-apply, and the query round trip (fixture table created, populated, read through RETURNING rows, dropped). Everything is named stackql-smoke-<stamp> / STACKQL_SMOKE_<stamp> and swept first. Free-tier cost: nothing. Statements are paced at 1.2 s; a 429 fails the run. The harness upgrades pystackql's managed stackql binary to >= v0.10.601 when older. Never run this against a production organization or project.
The live suite has not yet been run from this repository (no token in the build environment); NOTES.md lists what the first run establishes.
set -a; source .env; set +a
REG_ROOT="$(pwd)/provider-dev/openapi"
REG="{\"url\":\"file://${REG_ROOT}\",\"localDocRoot\":\"${REG_ROOT}\",\"verifyConfig\":{\"nopVerify\":true}}"
stackql --registry="${REG}" shell.github/workflows/build-and-test.yml: pin check + build + generation-drift check, offline validation, integration tests, meta-route tests and docs generation on every push and PR; the secret-gated live smoke suite (never the project lifecycle) on pushes; and a weekly spec-drift job that fetches the served spec, compares it with the pin, and opens a spec-drift issue when it moves. The web workflows build and deploy the microsite from main.
To publish, push the supabase dir to providers/src in a feature branch of the stackql-provider-registry and follow the registry release flow. Pull and verify from the dev registry:
export DEV_REG="{ \"url\": \"https://registry-dev.stackql.app/providers\" }"
stackql --registry="${DEV_REG}" shellregistry pull supabase;then make smoke-live.
The doc microsite (website/) is Docusaurus 3.10 and follows the shared architecture used by the other provider microsites: navbar/footer/theme/plugin configuration lives in stackql/docusaurus-config, vendored into .shared-config/ at build time. Site-local files are limited to the provider identity (website/provider.js), thin wrappers (docusaurus.config.js flips showLastUpdateTime on so every page carries a "Last updated on" stamp), the shared components/theme under src/, and static assets including static/CNAME (supabase-provider.stackql.io).
make docs # generate-docs --snake-case-aliases + website/scripts/sanitize-docs.mjs
make website # yarn install && yarn build (vendors the shared config; needs GitHub access)
make website-startheaderContent1.txt / headerContent2.txt in provider-dev/docgen/provider-data/ supply the landing page: installation, scope, token creation and the env var convention, project scope, the rate limit, beta labelling, and the example queries (estate inventory, the project security posture in four statements, control plane to Postgres rows in two statements, secrets and function inventory, branch hygiene, provisioning, and the serverless Postgres estate query alongside neon). sanitize-docs.mjs escapes MDX-hostile description text and annotates every generated ref example "required unless SUPABASE_PROJECT_ID is set".
To publish, select GitHub Actions as the Pages source and create the DNS record (the served hostname is pinned by website/static/CNAME):
| Source Domain | Record Type | Target |
|---|---|---|
| supabase-provider.stackql.io | CNAME | stackql.github.io. |
MIT - see LICENSE.
Issues and pull requests welcome. Regenerations must be followed by make test before commit; test harnesses must pace under the Management API rate limit and clean up everything they create.