diff --git a/.github/workflows/check-management-api-types.yml b/.github/workflows/check-management-api-types.yml
index 866274d1..d5e92043 100644
--- a/.github/workflows/check-management-api-types.yml
+++ b/.github/workflows/check-management-api-types.yml
@@ -20,7 +20,7 @@ jobs:
run: pnpm --filter @supabase/mcp-server-supabase generate:management-api-types
- name: Check for diff
run: |
- if ! git diff --exit-code packages/mcp-server-supabase/src/management-api/types.ts; then
+ if ! git diff --exit-code packages/mcp-server-supabase/src/management-api/types.ts packages/mcp-server-supabase/src/management-api/v2-types.ts; then
echo "Management API types are out of date. Run 'pnpm generate:management-api-types' to update."
exit 1
fi
diff --git a/packages/mcp-server-supabase/scripts/generate-management-api-types.mjs b/packages/mcp-server-supabase/scripts/generate-management-api-types.mjs
index 20975e34..ebf9ca45 100644
--- a/packages/mcp-server-supabase/scripts/generate-management-api-types.mjs
+++ b/packages/mcp-server-supabase/scripts/generate-management-api-types.mjs
@@ -1,8 +1,38 @@
+import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
import openapiTS, { astToString, UNKNOWN } from 'openapi-typescript';
-const SPEC_URL = 'https://api.supabase.com/api/v1-json';
-const OUTPUT_PATH = new URL('../src/management-api/types.ts', import.meta.url);
+const V1_SPEC_URL = 'https://api.supabase.com/api/v1-json';
+const V1_OUTPUT_PATH = new URL(
+ '../src/management-api/types.ts',
+ import.meta.url
+);
+// The v2 document is vendored rather than fetched: `/api/v2-json` is not
+// served yet, so a URL pull would make regeneration depend on a deployment.
+//
+// Provenance. The vendored file is generation input and is never hand-edited.
+// It is the Management API repo's `api/apps/mgmt-api/specs/v2.spec.json` at
+// rates head af464cca85, whose bytes hash to sha256
+// dfe848df7543e03d92f25c29a41829f980c99fdca8a326f5903aab1a83c70b26. The copy
+// here differs from those bytes in whitespace only, because this repo's
+// formatter checks every JSON file it can parse and a generated artifact that
+// fails the formatter would fail CI. The documents are equal as documents:
+// their canonical JSON (keys sorted, no insignificant whitespace) hashes to
+// sha256 10250c8bd4c29332f10cec5556ee6b5ebc8766f39f9f763cb2e3a0d3db71d1bb on
+// both sides.
+//
+// Refresh, once `/api/v2-json` is served: copy the served document over this
+// file, run `pnpm format`, then re-check content equality against the served
+// bytes the same way, canonically rather than byte for byte.
+const V2_SPEC_PATH = new URL(
+ './specs/management-api-v2.spec.json',
+ import.meta.url
+);
+const V2_OUTPUT_PATH = new URL(
+ '../src/management-api/v2-types.ts',
+ import.meta.url
+);
// Recursive "any JSON value" schemas produce a self-referential type that TypeScript
// rejects (TS2502). These fields are opaque blobs anyway, so `unknown` is fine.
@@ -14,13 +44,34 @@ function transform(schemaObject, options) {
return undefined;
}
-const ast = await openapiTS(SPEC_URL, { transform });
+/**
+ * Generates one document's types. Each API version owns its own module: the v1
+ * contract every current caller depends on stays exactly as it was, and v2
+ * arrives beside it.
+ */
+async function generate(spec, outputPath) {
+ const ast = await openapiTS(spec, { transform });
-const output = `/**
+ const output = `/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/
${astToString(ast)}`;
-writeFileSync(OUTPUT_PATH, output);
+ writeFileSync(outputPath, output);
+}
+
+await generate(V1_SPEC_URL, V1_OUTPUT_PATH);
+await generate(V2_SPEC_PATH, V2_OUTPUT_PATH);
+
+// The v1 output predates this repo's formatter and is excluded from it, so it
+// stays exactly as the generator emits it. The v2 output is new and is not
+// excluded, so it is formatted here instead: a freshly generated tree then
+// passes `pnpm format:check`, and regeneration stays reproducible because the
+// formatter is idempotent.
+execFileSync(
+ 'pnpm',
+ ['exec', 'biome', 'format', '--write', fileURLToPath(V2_OUTPUT_PATH)],
+ { stdio: 'inherit' }
+);
diff --git a/packages/mcp-server-supabase/scripts/specs/management-api-v2.spec.json b/packages/mcp-server-supabase/scripts/specs/management-api-v2.spec.json
new file mode 100644
index 00000000..b218638e
--- /dev/null
+++ b/packages/mcp-server-supabase/scripts/specs/management-api-v2.spec.json
@@ -0,0 +1,5464 @@
+{
+ "components": {
+ "schemas": {
+ "CreateLogDrainRequestOpenApi": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "backend_type": {
+ "enum": [
+ "axiom",
+ "bigquery",
+ "clickhouse",
+ "datadog",
+ "last9",
+ "loki",
+ "otlp",
+ "postgres",
+ "s3",
+ "sentry",
+ "syslog",
+ "webhook"
+ ],
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "hostname": {
+ "type": "string"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "port": {
+ "nullable": true,
+ "type": "number"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "url": {
+ "nullable": true,
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "postgres",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "gzip": {
+ "type": "boolean"
+ },
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "http": {
+ "enum": ["http1", "http2"],
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "title": "webhook",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dataset_id": {
+ "type": "string"
+ },
+ "project_id": {
+ "type": "string"
+ }
+ },
+ "title": "bigquery",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_key": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "title": "datadog",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "loki",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dsn": {
+ "type": "string"
+ }
+ },
+ "title": "sentry",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_token": {
+ "type": "string"
+ },
+ "dataset_name": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "string"
+ }
+ },
+ "title": "axiom",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ca_cert": {
+ "type": "string"
+ },
+ "cipher_key": {
+ "type": "string"
+ },
+ "client_cert": {
+ "type": "string"
+ },
+ "client_key": {
+ "type": "string"
+ },
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "maximum": 65535,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "structured_data": {
+ "type": "string"
+ },
+ "tls": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "title": "syslog",
+ "type": "object"
+ }
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["backend_type", "config", "name"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["log_drain"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "ErrorResponseBody": {
+ "properties": {
+ "error": {
+ "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject"
+ }
+ },
+ "required": ["error"],
+ "type": "object"
+ },
+ "ErrorResponseBodyAPIErrorObject": {
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "issues": {
+ "items": {
+ "$ref": "#/components/schemas/ErrorResponseBodyAPIErrorObject"
+ },
+ "type": "array"
+ },
+ "links": {
+ "additionalProperties": {
+ "properties": {
+ "describedby": {
+ "type": "string"
+ },
+ "href": {
+ "type": "string"
+ },
+ "meta": {
+ "additionalProperties": {},
+ "type": "object"
+ },
+ "rel": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ },
+ "required": ["href"],
+ "type": "object"
+ },
+ "type": "object"
+ },
+ "message": {
+ "type": "string"
+ },
+ "meta": {
+ "additionalProperties": {},
+ "type": "object"
+ }
+ },
+ "ref": "APIErrorObject",
+ "required": ["code", "message"],
+ "type": "object"
+ },
+ "ListLogDrainsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "backend_type": {
+ "enum": [
+ "axiom",
+ "bigquery",
+ "clickhouse",
+ "datadog",
+ "last9",
+ "loki",
+ "otlp",
+ "postgres",
+ "s3",
+ "sentry",
+ "syslog",
+ "webhook"
+ ],
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "hostname": {
+ "type": "string"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "port": {
+ "nullable": true,
+ "type": "number"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "url": {
+ "nullable": true,
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "postgres",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "gzip": {
+ "type": "boolean"
+ },
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "http": {
+ "enum": ["http1", "http2"],
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "title": "webhook",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dataset_id": {
+ "type": "string"
+ },
+ "project_id": {
+ "type": "string"
+ }
+ },
+ "title": "bigquery",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_key": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "title": "datadog",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "loki",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dsn": {
+ "type": "string"
+ }
+ },
+ "title": "sentry",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_token": {
+ "type": "string"
+ },
+ "dataset_name": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "string"
+ }
+ },
+ "title": "axiom",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ca_cert": {
+ "type": "string"
+ },
+ "cipher_key": {
+ "type": "string"
+ },
+ "client_cert": {
+ "type": "string"
+ },
+ "client_key": {
+ "type": "string"
+ },
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "maximum": 65535,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "structured_data": {
+ "type": "string"
+ },
+ "tls": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "title": "syslog",
+ "type": "object"
+ }
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["backend_type", "config", "name"],
+ "type": "object"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["log_drain"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "LogDrainResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "backend_type": {
+ "enum": [
+ "axiom",
+ "bigquery",
+ "clickhouse",
+ "datadog",
+ "last9",
+ "loki",
+ "otlp",
+ "postgres",
+ "s3",
+ "sentry",
+ "syslog",
+ "webhook"
+ ],
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "hostname": {
+ "type": "string"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "port": {
+ "nullable": true,
+ "type": "number"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "url": {
+ "nullable": true,
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "postgres",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "gzip": {
+ "type": "boolean"
+ },
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "http": {
+ "enum": ["http1", "http2"],
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "title": "webhook",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dataset_id": {
+ "type": "string"
+ },
+ "project_id": {
+ "type": "string"
+ }
+ },
+ "title": "bigquery",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_key": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "title": "datadog",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "loki",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dsn": {
+ "type": "string"
+ }
+ },
+ "title": "sentry",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_token": {
+ "type": "string"
+ },
+ "dataset_name": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "string"
+ }
+ },
+ "title": "axiom",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ca_cert": {
+ "type": "string"
+ },
+ "cipher_key": {
+ "type": "string"
+ },
+ "client_cert": {
+ "type": "string"
+ },
+ "client_key": {
+ "type": "string"
+ },
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "maximum": 65535,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "structured_data": {
+ "type": "string"
+ },
+ "tls": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "title": "syslog",
+ "type": "object"
+ }
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["backend_type", "config", "name"],
+ "type": "object"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["log_drain"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "OrganizationMemberRoleResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "name": {
+ "description": "Role name. For project-scoped assignments this is the base role name.",
+ "example": "developer",
+ "type": "string"
+ },
+ "projects": {
+ "description": "Project refs this role is scoped to. Empty array for org-level roles.",
+ "items": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "ref": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "ref"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "scope": {
+ "description": "Whether this role applies org-wide or is scoped to specific projects for the user.",
+ "enum": ["organization", "project"],
+ "type": "string"
+ }
+ },
+ "required": ["name", "projects", "scope"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_member_role"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "UpdateLogDrainRequestOpenApi": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "backend_type": {
+ "enum": [
+ "axiom",
+ "bigquery",
+ "clickhouse",
+ "datadog",
+ "last9",
+ "loki",
+ "otlp",
+ "postgres",
+ "s3",
+ "sentry",
+ "syslog",
+ "webhook"
+ ],
+ "type": "string"
+ },
+ "config": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "hostname": {
+ "type": "string"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "port": {
+ "nullable": true,
+ "type": "number"
+ },
+ "schema": {
+ "type": "string"
+ },
+ "url": {
+ "nullable": true,
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "postgres",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "gzip": {
+ "type": "boolean"
+ },
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "http": {
+ "enum": ["http1", "http2"],
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ },
+ "title": "webhook",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dataset_id": {
+ "type": "string"
+ },
+ "project_id": {
+ "type": "string"
+ }
+ },
+ "title": "bigquery",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_key": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "title": "datadog",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "password": {
+ "nullable": true,
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "username": {
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "title": "loki",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dsn": {
+ "type": "string"
+ }
+ },
+ "title": "sentry",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "api_token": {
+ "type": "string"
+ },
+ "dataset_name": {
+ "type": "string"
+ },
+ "domain": {
+ "type": "string"
+ }
+ },
+ "title": "axiom",
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ca_cert": {
+ "type": "string"
+ },
+ "cipher_key": {
+ "type": "string"
+ },
+ "client_cert": {
+ "type": "string"
+ },
+ "client_key": {
+ "type": "string"
+ },
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "maximum": 65535,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "structured_data": {
+ "type": "string"
+ },
+ "tls": {
+ "default": false,
+ "type": "boolean"
+ }
+ },
+ "title": "syslog",
+ "type": "object"
+ }
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["backend_type"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["log_drain"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2AssignOrganizationMemberRoleRequest": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "projects": {
+ "description": "The projects to assign a project-scoped role for. If omitted, assigns an org-wide role.",
+ "items": {
+ "properties": {
+ "ref": {
+ "description": "Project ref",
+ "example": "abcjuqabhgwjjutfvtpa",
+ "type": "string"
+ }
+ },
+ "required": ["ref"],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "role": {
+ "description": "Role name to assign. Must be one of: owner, administrator, developer, read-only. Must be on a Team or Enterprise plan to use the read-only role.",
+ "enum": [
+ "administrator",
+ "developer",
+ "owner",
+ "read-only"
+ ],
+ "example": "developer",
+ "type": "string"
+ }
+ },
+ "required": ["role"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_member_role"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2BranchCreationRateResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "additionalProperties": {},
+ "properties": {
+ "amount": {
+ "description": "Authoritative rate a new branch adds to the organization. Zero is authoritative and means the branch can be created without an additional charge.",
+ "example": 0.01344,
+ "type": "number"
+ },
+ "currency": {
+ "description": "ISO 4217 currency of the amount.",
+ "example": "USD",
+ "type": "string"
+ },
+ "organization_slug": {
+ "description": "Organization the charge lands on.",
+ "example": "my-org",
+ "type": "string"
+ },
+ "parent_project_ref": {
+ "description": "Project the branch would be created under.",
+ "example": "abcdefghijklmnopqrst",
+ "type": "string"
+ },
+ "plan_id": {
+ "description": "Plan that decided the amount.",
+ "example": "pro",
+ "type": "string"
+ },
+ "recurrence": {
+ "description": "Interval the amount recurs at.",
+ "enum": ["hourly", "monthly"],
+ "example": "hourly",
+ "type": "string"
+ }
+ },
+ "required": [
+ "amount",
+ "currency",
+ "organization_slug",
+ "parent_project_ref",
+ "plan_id",
+ "recurrence"
+ ],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["branch_creation_rate"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2CreateInvitationsRequest": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ },
+ "projects": {
+ "description": "The projects to limit a user to. If omitted, user will have org-wide access with the provided role.",
+ "items": {
+ "properties": {
+ "ref": {
+ "description": "Project ref",
+ "example": "abcjuqabhgwjjutfvtpa",
+ "type": "string"
+ }
+ },
+ "required": ["ref"],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "require_sso": {
+ "type": "boolean"
+ },
+ "role": {
+ "description": "Role name to assign. Must be on a Team or Enterprise plan to use the read-only role.",
+ "enum": [
+ "administrator",
+ "developer",
+ "owner",
+ "read-only"
+ ],
+ "example": "developer",
+ "type": "string"
+ }
+ },
+ "required": ["email", "role"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_invitation"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ },
+ "maxItems": 50,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2CreateInvitationsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ }
+ },
+ "required": ["email"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_invitation"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "error": {
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "issues": {
+ "items": {
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "links": {
+ "additionalProperties": {
+ "properties": {
+ "describedby": {
+ "type": "string"
+ },
+ "href": {
+ "type": "string"
+ },
+ "meta": {
+ "additionalProperties": {},
+ "type": "object"
+ },
+ "rel": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ },
+ "required": ["href"],
+ "type": "object"
+ },
+ "type": "object"
+ },
+ "message": {
+ "type": "string"
+ },
+ "meta": {
+ "properties": {
+ "email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ }
+ },
+ "required": ["email"],
+ "type": "object"
+ }
+ },
+ "required": ["code", "message", "meta"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "links": {
+ "additionalProperties": {
+ "properties": {
+ "describedby": {
+ "type": "string"
+ },
+ "href": {
+ "type": "string"
+ },
+ "meta": {
+ "additionalProperties": {},
+ "type": "object"
+ },
+ "rel": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ }
+ },
+ "required": ["href"],
+ "type": "object"
+ },
+ "type": "object"
+ },
+ "message": {
+ "type": "string"
+ },
+ "meta": {
+ "additionalProperties": {},
+ "type": "object"
+ }
+ },
+ "required": ["code", "message"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2CreatePrivateLinkAssociationRequest": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "account_name": {
+ "description": "Optional human-readable name for the AWS account.",
+ "maxLength": 128,
+ "type": "string"
+ },
+ "aws_account_id": {
+ "description": "The AWS account ID to add to the project PrivateLink share.",
+ "maxLength": 12,
+ "minLength": 12,
+ "pattern": "^\\d{12}$",
+ "type": "string"
+ },
+ "database_identifier": {
+ "description": "Identifier of the read replica this PrivateLink share should target. Omit to target the primary database.",
+ "type": "string"
+ }
+ },
+ "required": ["aws_account_id"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["private_link_association"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2DeleteInvitationsRequest": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ }
+ },
+ "required": ["email"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_invitation"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ },
+ "maxItems": 100,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2DeleteInvitationsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ }
+ },
+ "required": ["email"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_invitation"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2DeployWorkerRequest": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "context_upload_id": {
+ "description": "Id of a build context staged through the uploads endpoint. Required unless `runtime` is set.",
+ "type": "string"
+ },
+ "spec": {
+ "properties": {
+ "exposure": {
+ "example": "public",
+ "type": "string"
+ },
+ "instances": {
+ "example": 1,
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "runtime": {
+ "example": "node",
+ "type": "string"
+ },
+ "size": {
+ "example": "2gb-1vcpu",
+ "type": "string"
+ }
+ },
+ "required": ["exposure", "instances", "size"],
+ "type": "object"
+ }
+ },
+ "required": ["spec"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_worker"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2ListGitHubConnectionsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "branch_limit": {
+ "description": "Maximum number of preview branches",
+ "type": "number"
+ },
+ "inserted_at": {
+ "description": "When the connection was created",
+ "type": "string"
+ },
+ "installation_id": {
+ "description": "GitHub App installation id",
+ "type": "number"
+ },
+ "new_branch_per_pr": {
+ "description": "Whether a preview branch is created for every pull request",
+ "type": "boolean"
+ },
+ "project": {
+ "description": "The connected Supabase project",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ },
+ "ref": {
+ "description": "Project ref",
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ },
+ "required": ["id", "name", "ref"],
+ "type": "object"
+ },
+ "repository": {
+ "description": "The connected GitHub repository",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "name"],
+ "type": "object"
+ },
+ "supabase_changes_only": {
+ "description": "Whether branches are only created for changes under `supabase/`",
+ "type": "boolean"
+ },
+ "updated_at": {
+ "description": "When the connection was last updated",
+ "type": "string"
+ },
+ "user": {
+ "description": "The user who created the connection, if still known",
+ "nullable": true,
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "primary_email": {
+ "nullable": true,
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ },
+ "required": ["id", "primary_email", "username"],
+ "type": "object"
+ },
+ "workdir": {
+ "description": "Directory within the repository the project lives in",
+ "type": "string"
+ }
+ },
+ "required": [
+ "branch_limit",
+ "inserted_at",
+ "installation_id",
+ "new_branch_per_pr",
+ "project",
+ "repository",
+ "supabase_changes_only",
+ "updated_at",
+ "user",
+ "workdir"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "description": "Connection id.",
+ "example": "7",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["github_connection"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "links": {
+ "properties": {
+ "first": {
+ "description": "URL path to the first page if available.",
+ "example": "/v2/organizations/my-org/integrations/github/connections?page[size]=10",
+ "nullable": true,
+ "type": "string"
+ },
+ "last": {
+ "description": "URL path to the last page if available.",
+ "example": "/v2/organizations/my-org/integrations/github/connections?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295",
+ "nullable": true,
+ "type": "string"
+ },
+ "next": {
+ "description": "URL path to the next page.",
+ "example": "/v2/organizations/my-org/integrations/github/connections?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4",
+ "nullable": true,
+ "type": "string"
+ },
+ "prev": {
+ "description": "URL path to the previous page.",
+ "example": "/v2/organizations/my-org/integrations/github/connections?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7",
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "required": ["next", "prev"],
+ "type": "object"
+ }
+ },
+ "required": ["data", "links"],
+ "type": "object"
+ },
+ "V2ListMembersResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "avatar_url": {
+ "description": "Member's avatar URL",
+ "nullable": true,
+ "type": "string"
+ },
+ "is_sso_user": {
+ "description": "Whether this member is a Single Sign-On user",
+ "type": "boolean"
+ },
+ "mfa_enabled": {
+ "description": "Whether Multi-Factor Authentication is enabled for this member",
+ "type": "boolean"
+ },
+ "primary_email": {
+ "description": "Member's primary email",
+ "nullable": true,
+ "type": "string"
+ },
+ "roles": {
+ "description": "Roles assigned to this member. Includes both org-level and project-scoped roles.",
+ "items": {
+ "properties": {
+ "name": {
+ "description": "Role name. For project-scoped roles this is the base role name.",
+ "example": "developer",
+ "type": "string"
+ },
+ "projects": {
+ "description": "Project refs this role is scoped to. Empty array for org-level roles.",
+ "items": {
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "ref": {
+ "type": "string"
+ }
+ },
+ "required": ["name", "ref"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "scope": {
+ "description": "Whether this role applies org-wide or is scoped to specific projects for the user.",
+ "enum": ["organization", "project"],
+ "type": "string"
+ }
+ },
+ "required": ["name", "projects", "scope"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "username": {
+ "description": "Member's username",
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "required": [
+ "avatar_url",
+ "is_sso_user",
+ "mfa_enabled",
+ "primary_email",
+ "roles",
+ "username"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_member"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "links": {
+ "properties": {
+ "first": {
+ "description": "URL path to the first page if available.",
+ "example": "/v2/organizations/my-org/members?page[size]=10",
+ "nullable": true,
+ "type": "string"
+ },
+ "last": {
+ "description": "URL path to the last page if available.",
+ "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295",
+ "nullable": true,
+ "type": "string"
+ },
+ "next": {
+ "description": "URL path to the next page.",
+ "example": "/v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4",
+ "nullable": true,
+ "type": "string"
+ },
+ "prev": {
+ "description": "URL path to the previous page.",
+ "example": "/v2/organizations/my-org/members?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7",
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "required": ["next", "prev"],
+ "type": "object"
+ }
+ },
+ "required": ["data", "links"],
+ "type": "object"
+ },
+ "V2ListPrivateLinkAssociationsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "account_name": {
+ "description": "Human-readable name for the AWS account.",
+ "type": "string"
+ },
+ "aws_account_id": {
+ "description": "The AWS account ID this PrivateLink share is associated with.",
+ "maxLength": 12,
+ "minLength": 12,
+ "pattern": "^\\d{12}$",
+ "type": "string"
+ },
+ "database_identifier": {
+ "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier.",
+ "type": "string"
+ },
+ "database_type": {
+ "description": "Whether this PrivateLink share targets the primary database or a read replica.",
+ "enum": ["PRIMARY", "READ_REPLICA"],
+ "type": "string"
+ },
+ "resource_access_manager_resource_config_arn": {
+ "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.",
+ "type": "string"
+ },
+ "resource_access_manager_resource_config_id": {
+ "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.",
+ "type": "string"
+ },
+ "resource_access_manager_share_arn": {
+ "description": "ARN of the AWS Resource Access Manager resource share for this association.",
+ "type": "string"
+ },
+ "shared_at": {
+ "description": "The time and date at which the AWS Resource Share Association was requested from Supabase. `null` means that the association was not yet requested while the PrivateLink Association is pending.",
+ "format": "date-time",
+ "nullable": true,
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "type": "string"
+ },
+ "status": {
+ "description": "\n - `CREATING`: The PrivateLink resources and the Association are in the process of being created. The PrivateLink Share cannot be accepted yet.\n - `READY`: The PrivateLink resources have been created and the PrivateLink Share can be accepted for the duration of 12h after sharing. See `shared_at`.\n - `ASSOCIATION_REQUEST_EXPIRED`: The PrivateLink Share has not been accepted within the 12h time limit. This association can now be deleted.\n - `ASSOCIATION_ACCEPTED`: The PrivateLink Share was successfully accepted.\n - `CREATION_FAILED`: The PrivateLink resources failed to create. This likely means something went wrong on Supabase's and and support should be contacted.\n - `DELETING`: The PrivateLink resources and the Association are in the process of being deleted. The PrivateLink Share cannot be accepted yet.\n",
+ "enum": [
+ "ASSOCIATION_ACCEPTED",
+ "ASSOCIATION_REQUEST_EXPIRED",
+ "CREATING",
+ "CREATION_FAILED",
+ "DELETING",
+ "READY"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "aws_account_id",
+ "database_identifier",
+ "database_type",
+ "shared_at",
+ "status"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["private_link_association"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2ListProjectsResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "cloud_provider": {
+ "description": "Cloud provider hosting the project",
+ "type": "string"
+ },
+ "databases": {
+ "description": "The project's databases including compute and disk attributes.",
+ "items": {
+ "properties": {
+ "cloud_provider": {
+ "type": "string"
+ },
+ "disk_last_modified_at": {
+ "type": "string"
+ },
+ "disk_throughput_mbps": {
+ "type": "number"
+ },
+ "disk_type": {
+ "enum": ["gp3", "io2"],
+ "type": "string"
+ },
+ "disk_volume_size_gb": {
+ "type": "number"
+ },
+ "identifier": {
+ "type": "string"
+ },
+ "infra_compute_size": {
+ "enum": [
+ "12xlarge",
+ "16xlarge",
+ "24xlarge",
+ "24xlarge_high_memory",
+ "24xlarge_optimized_cpu",
+ "24xlarge_optimized_memory",
+ "2xlarge",
+ "48xlarge",
+ "48xlarge_high_memory",
+ "48xlarge_optimized_cpu",
+ "48xlarge_optimized_memory",
+ "4xlarge",
+ "8xlarge",
+ "large",
+ "medium",
+ "micro",
+ "nano",
+ "pico",
+ "small",
+ "xlarge"
+ ],
+ "type": "string"
+ },
+ "region": {
+ "nullable": true,
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "ACTIVE_HEALTHY",
+ "ACTIVE_UNHEALTHY",
+ "COMING_UP",
+ "GOING_DOWN",
+ "INIT_FAILED",
+ "INIT_READ_REPLICA",
+ "INIT_READ_REPLICA_FAILED",
+ "REMOVED",
+ "RESIZING",
+ "RESTARTING",
+ "RESTORING",
+ "UNKNOWN"
+ ],
+ "type": "string"
+ },
+ "type": {
+ "enum": ["PRIMARY", "READ_REPLICA"],
+ "type": "string"
+ }
+ },
+ "required": [
+ "cloud_provider",
+ "identifier",
+ "region",
+ "status",
+ "type"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "inserted_at": {
+ "description": "When the project was created",
+ "type": "string"
+ },
+ "name": {
+ "description": "Project name",
+ "type": "string"
+ },
+ "region": {
+ "description": "Region the project is hosted in",
+ "type": "string"
+ },
+ "status": {
+ "description": "Project status",
+ "enum": [
+ "ACTIVE_HEALTHY",
+ "ACTIVE_UNHEALTHY",
+ "COMING_UP",
+ "GOING_DOWN",
+ "INACTIVE",
+ "INIT_FAILED",
+ "PAUSE_FAILED",
+ "PAUSING",
+ "REMOVED",
+ "RESIZING",
+ "RESTARTING",
+ "RESTORE_FAILED",
+ "RESTORING",
+ "UNKNOWN",
+ "UPGRADING"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "cloud_provider",
+ "databases",
+ "inserted_at",
+ "name",
+ "region",
+ "status"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "description": "Project ref",
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "links": {
+ "properties": {
+ "first": {
+ "description": "URL path to the first page if available.",
+ "example": "/v2/organizations/my-org/projects?page[size]=10",
+ "nullable": true,
+ "type": "string"
+ },
+ "last": {
+ "description": "URL path to the last page if available.",
+ "example": "/v2/organizations/my-org/projects?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295",
+ "nullable": true,
+ "type": "string"
+ },
+ "next": {
+ "description": "URL path to the next page.",
+ "example": "/v2/organizations/my-org/projects?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4",
+ "nullable": true,
+ "type": "string"
+ },
+ "prev": {
+ "description": "URL path to the previous page.",
+ "example": "/v2/organizations/my-org/projects?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7",
+ "nullable": true,
+ "type": "string"
+ }
+ },
+ "required": ["next", "prev"],
+ "type": "object"
+ }
+ },
+ "required": ["data", "links"],
+ "type": "object"
+ },
+ "V2ListRolesResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "name": {
+ "description": "Role name.",
+ "example": "developer",
+ "type": "string"
+ }
+ },
+ "required": ["name"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["organization_role"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2ListWorkersResponse": {
+ "properties": {
+ "data": {
+ "items": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "build_state": {
+ "enum": ["active", "building", "failed"],
+ "type": "string"
+ },
+ "deleting": {
+ "type": "boolean"
+ },
+ "image_version": {
+ "type": "string"
+ },
+ "instances": {
+ "properties": {
+ "declared": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "live": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "ready": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "stale": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": ["declared", "live", "ready", "stale"],
+ "type": "object"
+ },
+ "instances_error": {
+ "type": "string"
+ },
+ "secret_generation": {
+ "type": "string"
+ },
+ "spec": {
+ "properties": {
+ "exposure": {
+ "example": "public",
+ "type": "string"
+ },
+ "instances": {
+ "example": 1,
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "runtime": {
+ "example": "node",
+ "type": "string"
+ },
+ "size": {
+ "example": "2gb-1vcpu",
+ "type": "string"
+ }
+ },
+ "required": ["exposure", "instances", "size"],
+ "type": "object"
+ },
+ "state_reason": {
+ "type": "string"
+ }
+ },
+ "required": ["build_state", "secret_generation", "spec"],
+ "type": "object"
+ },
+ "id": {
+ "description": "Worker name.",
+ "example": "hello-world",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_worker"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2PreviewProjectTransferResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "errors": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["key", "message"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "info": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["key", "message"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "valid": {
+ "type": "boolean"
+ },
+ "warnings": {
+ "items": {
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": ["key", "message"],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": ["errors", "info", "valid", "warnings"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_transfer_result"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2PrivateLinkAssociationResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "account_name": {
+ "description": "Human-readable name for the AWS account.",
+ "type": "string"
+ },
+ "aws_account_id": {
+ "description": "The AWS account ID this PrivateLink share is associated with.",
+ "maxLength": 12,
+ "minLength": 12,
+ "pattern": "^\\d{12}$",
+ "type": "string"
+ },
+ "database_identifier": {
+ "description": "Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier.",
+ "type": "string"
+ },
+ "database_type": {
+ "description": "Whether this PrivateLink share targets the primary database or a read replica.",
+ "enum": ["PRIMARY", "READ_REPLICA"],
+ "type": "string"
+ },
+ "resource_access_manager_resource_config_arn": {
+ "description": "ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share.",
+ "type": "string"
+ },
+ "resource_access_manager_resource_config_id": {
+ "description": "ID of the AWS VPC Lattice resource configuration backing this PrivateLink share.",
+ "type": "string"
+ },
+ "resource_access_manager_share_arn": {
+ "description": "ARN of the AWS Resource Access Manager resource share for this association.",
+ "type": "string"
+ },
+ "shared_at": {
+ "description": "The time and date at which the AWS Resource Share Association was requested from Supabase. `null` means that the association was not yet requested while the PrivateLink Association is pending.",
+ "format": "date-time",
+ "nullable": true,
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
+ "type": "string"
+ },
+ "status": {
+ "description": "\n - `CREATING`: The PrivateLink resources and the Association are in the process of being created. The PrivateLink Share cannot be accepted yet.\n - `READY`: The PrivateLink resources have been created and the PrivateLink Share can be accepted for the duration of 12h after sharing. See `shared_at`.\n - `ASSOCIATION_REQUEST_EXPIRED`: The PrivateLink Share has not been accepted within the 12h time limit. This association can now be deleted.\n - `ASSOCIATION_ACCEPTED`: The PrivateLink Share was successfully accepted.\n - `CREATION_FAILED`: The PrivateLink resources failed to create. This likely means something went wrong on Supabase's and and support should be contacted.\n - `DELETING`: The PrivateLink resources and the Association are in the process of being deleted. The PrivateLink Share cannot be accepted yet.\n",
+ "enum": [
+ "ASSOCIATION_ACCEPTED",
+ "ASSOCIATION_REQUEST_EXPIRED",
+ "CREATING",
+ "CREATION_FAILED",
+ "DELETING",
+ "READY"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "aws_account_id",
+ "database_identifier",
+ "database_type",
+ "shared_at",
+ "status"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["private_link_association"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2ProjectConfigResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "api": {
+ "properties": {
+ "db_extra_search_path": {
+ "type": "string"
+ },
+ "db_pool": {
+ "description": "If `null`, no pool size is written to the project's PostgREST config and PostgREST's own default applies. The platform does not pick a value here.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "nullable": true,
+ "type": "integer"
+ },
+ "db_pool_acquisition_timeout": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "db_schema": {
+ "description": "Schemas exposed through the Data API",
+ "type": "string"
+ },
+ "max_rows": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "db_extra_search_path",
+ "db_pool",
+ "db_pool_acquisition_timeout",
+ "db_schema",
+ "max_rows"
+ ],
+ "type": "object"
+ },
+ "auth": {
+ "additionalProperties": {},
+ "description": "Effective Auth config, keyed by lowercased GoTrue setting name and resolved through the `gotrue_config` view, so a setting the project has never overridden is reported at its platform default. Secrets are returned as an HMAC of their value, never in plaintext.",
+ "type": "object"
+ },
+ "database": {
+ "properties": {
+ "major_version": {
+ "description": "The major Postgres version the database runs. `17` covers both Postgres 17 and Oriole on 17, since Oriole is a storage engine rather than a version.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "network_restrictions": {
+ "properties": {
+ "allowed_cidrs": {
+ "items": {
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "type": {
+ "enum": ["v4", "v6"],
+ "type": "string"
+ }
+ },
+ "required": ["address", "type"],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "applied_at": {
+ "type": "string"
+ },
+ "entitlement": {
+ "enum": ["allowed", "disallowed"],
+ "type": "string"
+ },
+ "status": {
+ "description": "Whether the allowlist below is applied to the project or only stored.",
+ "enum": ["applied", "stored"],
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ },
+ "required": ["allowed_cidrs", "entitlement", "status"],
+ "type": "object"
+ },
+ "postgres_settings": {
+ "description": "Postgres parameter overrides. Empty when the project runs entirely on defaults.",
+ "properties": {
+ "checkpoint_timeout": {
+ "description": "Default unit: s",
+ "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$",
+ "type": "string"
+ },
+ "cron_log_statement": {
+ "type": "boolean"
+ },
+ "effective_cache_size": {
+ "type": "string"
+ },
+ "hot_standby_feedback": {
+ "type": "boolean"
+ },
+ "log_autovacuum_min_duration": {
+ "description": "Default unit: ms",
+ "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$",
+ "type": "string"
+ },
+ "log_checkpoints": {
+ "type": "boolean"
+ },
+ "log_connections": {
+ "type": "boolean"
+ },
+ "log_disconnections": {
+ "type": "boolean"
+ },
+ "log_duration": {
+ "type": "boolean"
+ },
+ "log_lock_waits": {
+ "type": "boolean"
+ },
+ "log_recovery_conflict_waits": {
+ "type": "boolean"
+ },
+ "log_replication_commands": {
+ "type": "boolean"
+ },
+ "log_startup_progress_interval": {
+ "description": "Default unit: ms",
+ "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$",
+ "type": "string"
+ },
+ "log_temp_files": {
+ "type": "string"
+ },
+ "logical_decoding_work_mem": {
+ "type": "string"
+ },
+ "maintenance_work_mem": {
+ "type": "string"
+ },
+ "max_connections": {
+ "maximum": 262143,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "max_locks_per_transaction": {
+ "maximum": 2147483640,
+ "minimum": 10,
+ "type": "integer"
+ },
+ "max_logical_replication_workers": {
+ "maximum": 262143,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "max_parallel_maintenance_workers": {
+ "maximum": 1024,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "max_parallel_workers": {
+ "maximum": 1024,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "max_parallel_workers_per_gather": {
+ "maximum": 1024,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "max_replication_slots": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_slot_wal_keep_size": {
+ "type": "string"
+ },
+ "max_standby_archive_delay": {
+ "type": "string"
+ },
+ "max_standby_streaming_delay": {
+ "type": "string"
+ },
+ "max_sync_workers_per_subscription": {
+ "maximum": 262143,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "max_wal_senders": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_wal_size": {
+ "type": "string"
+ },
+ "max_worker_processes": {
+ "maximum": 262143,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "session_replication_role": {
+ "enum": ["local", "origin", "replica"],
+ "type": "string"
+ },
+ "shared_buffers": {
+ "type": "string"
+ },
+ "statement_timeout": {
+ "description": "Default unit: ms",
+ "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$",
+ "type": "string"
+ },
+ "track_activity_query_size": {
+ "type": "string"
+ },
+ "track_commit_timestamp": {
+ "type": "boolean"
+ },
+ "wal_keep_size": {
+ "type": "string"
+ },
+ "wal_sender_timeout": {
+ "description": "Default unit: ms",
+ "pattern": "^(-?[0-9]+(?:\\.[0-9]+)?)(us|ms|s|min|h|d)?$",
+ "type": "string"
+ },
+ "work_mem": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "ssl_enforced": {
+ "description": "Whether the database rejects plaintext connections",
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "major_version",
+ "network_restrictions",
+ "postgres_settings",
+ "ssl_enforced"
+ ],
+ "type": "object"
+ },
+ "pooler": {
+ "properties": {
+ "default_pool_size": {
+ "description": "Defaults to the pooler's size for the project's compute when not overridden.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "ignore_startup_parameters": {
+ "type": "string"
+ },
+ "max_client_conn": {
+ "description": "Defaults to the pooler's size for the project's compute when not overridden.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "pool_mode": {
+ "enum": ["session", "statement", "transaction"],
+ "type": "string"
+ },
+ "query_wait_timeout": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "reserve_pool_size": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "server_idle_timeout": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "server_lifetime": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "default_pool_size",
+ "ignore_startup_parameters",
+ "max_client_conn",
+ "pool_mode",
+ "query_wait_timeout",
+ "reserve_pool_size",
+ "server_idle_timeout",
+ "server_lifetime"
+ ],
+ "type": "object"
+ },
+ "realtime": {
+ "properties": {
+ "connection_pool": {
+ "description": "Defaults to Realtime's pool size for the project's compute when not overridden.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_bytes_per_second": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_channels_per_client": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_concurrent_users": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_events_per_second": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_joins_per_second": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_payload_size_in_kb": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_presence_events_per_second": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "postgres_changes_pool": {
+ "description": "If `null`, no override is stored and Realtime applies its own default.",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "nullable": true,
+ "type": "integer"
+ },
+ "presence_enabled": {
+ "type": "boolean"
+ },
+ "private_only": {
+ "type": "boolean"
+ },
+ "suspend": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "connection_pool",
+ "max_bytes_per_second",
+ "max_channels_per_client",
+ "max_concurrent_users",
+ "max_events_per_second",
+ "max_joins_per_second",
+ "max_payload_size_in_kb",
+ "max_presence_events_per_second",
+ "postgres_changes_pool",
+ "presence_enabled",
+ "private_only",
+ "suspend"
+ ],
+ "type": "object"
+ },
+ "storage": {
+ "description": "Read from the storage service's admin API rather than the middleware DB, so unlike the rest of this resource it reflects the tenant's live config.",
+ "properties": {
+ "capabilities": {
+ "properties": {
+ "iceberg_catalog": {
+ "type": "boolean"
+ },
+ "list_v2": {
+ "type": "boolean"
+ }
+ },
+ "required": ["iceberg_catalog", "list_v2"],
+ "type": "object"
+ },
+ "database_pool_mode": {
+ "type": "string"
+ },
+ "features": {
+ "properties": {
+ "iceberg_catalog": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "max_catalogs": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_namespaces": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_tables": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "enabled",
+ "max_catalogs",
+ "max_namespaces",
+ "max_tables"
+ ],
+ "type": "object"
+ },
+ "image_transformation": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "purge_cache": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "s3_protocol": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ },
+ "required": ["enabled"],
+ "type": "object"
+ },
+ "vector_buckets": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "max_buckets": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "max_indexes": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "enabled",
+ "max_buckets",
+ "max_indexes"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "iceberg_catalog",
+ "image_transformation",
+ "purge_cache",
+ "s3_protocol",
+ "vector_buckets"
+ ],
+ "type": "object"
+ },
+ "file_size_limit": {
+ "format": "int64",
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "migration_version": {
+ "type": "string"
+ },
+ "upstream_target": {
+ "enum": ["canary", "main"],
+ "type": "string"
+ }
+ },
+ "required": [
+ "capabilities",
+ "database_pool_mode",
+ "features",
+ "file_size_limit",
+ "migration_version",
+ "upstream_target"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "api",
+ "auth",
+ "database",
+ "pooler",
+ "realtime",
+ "storage"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "description": "Project ref.",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_config"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2ProjectCreationRateResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "additionalProperties": {},
+ "properties": {
+ "active_project_count": {
+ "description": "Active projects already in the organization. On a paid plan the first one is absorbed by the organization's compute credits, so this decides whether the amount is zero.",
+ "example": 1,
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "amount": {
+ "description": "Authoritative rate a new project adds to the organization. Zero is authoritative and means the project can be created without an additional charge.",
+ "example": 10,
+ "type": "number"
+ },
+ "currency": {
+ "description": "ISO 4217 currency of the amount.",
+ "example": "USD",
+ "type": "string"
+ },
+ "organization_slug": {
+ "description": "Organization the charge lands on.",
+ "example": "my-org",
+ "type": "string"
+ },
+ "plan_id": {
+ "description": "Plan that decided the amount.",
+ "example": "pro",
+ "type": "string"
+ },
+ "recurrence": {
+ "description": "Interval the amount recurs at.",
+ "enum": ["hourly", "monthly"],
+ "example": "monthly",
+ "type": "string"
+ }
+ },
+ "required": [
+ "active_project_count",
+ "amount",
+ "currency",
+ "organization_slug",
+ "plan_id",
+ "recurrence"
+ ],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_creation_rate"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2TransferProjectBody": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "target_organization_slug": {
+ "type": "string"
+ }
+ },
+ "required": ["target_organization_slug"],
+ "type": "object"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_transfer_input"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2WorkerResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "build_state": {
+ "enum": ["active", "building", "failed"],
+ "type": "string"
+ },
+ "deleting": {
+ "type": "boolean"
+ },
+ "image_version": {
+ "type": "string"
+ },
+ "instances": {
+ "properties": {
+ "declared": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "live": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "ready": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "stale": {
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ }
+ },
+ "required": ["declared", "live", "ready", "stale"],
+ "type": "object"
+ },
+ "instances_error": {
+ "type": "string"
+ },
+ "secret_generation": {
+ "type": "string"
+ },
+ "spec": {
+ "properties": {
+ "exposure": {
+ "example": "public",
+ "type": "string"
+ },
+ "instances": {
+ "example": 1,
+ "maximum": 9007199254740991,
+ "minimum": -9007199254740991,
+ "type": "integer"
+ },
+ "runtime": {
+ "example": "node",
+ "type": "string"
+ },
+ "size": {
+ "example": "2gb-1vcpu",
+ "type": "string"
+ }
+ },
+ "required": ["exposure", "instances", "size"],
+ "type": "object"
+ },
+ "state_reason": {
+ "type": "string"
+ }
+ },
+ "required": ["build_state", "secret_generation", "spec"],
+ "type": "object"
+ },
+ "id": {
+ "description": "Worker name.",
+ "example": "hello-world",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_worker"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ },
+ "V2WorkerUploadResponse": {
+ "properties": {
+ "data": {
+ "properties": {
+ "attributes": {
+ "properties": {
+ "expires_at": {
+ "description": "When the slot stops accepting the upload.",
+ "type": "string"
+ },
+ "method": {
+ "example": "PUT",
+ "type": "string"
+ },
+ "url": {
+ "description": "Presigned destination for the `.tar.gz` build context.",
+ "type": "string"
+ }
+ },
+ "required": ["expires_at", "method", "url"],
+ "type": "object"
+ },
+ "id": {
+ "description": "Upload id to pass to the deploy endpoint as `context_upload_id`.",
+ "example": "cafe0000000000000000000000000000",
+ "type": "string"
+ },
+ "type": {
+ "description": "Resource type.",
+ "enum": ["project_worker_upload"],
+ "type": "string"
+ }
+ },
+ "required": ["attributes", "id", "type"],
+ "type": "object"
+ }
+ },
+ "required": ["data"],
+ "type": "object"
+ }
+ },
+ "securitySchemes": {
+ "bearer": {
+ "bearerFormat": "JWT",
+ "scheme": "bearer",
+ "type": "http"
+ }
+ }
+ },
+ "info": {
+ "contact": {},
+ "description": "Supabase API generated from the OpenAPI specification.
Visit [https://supabase.com/docs](https://supabase.com/docs) for a complete documentation.",
+ "title": "Supabase API (v2)",
+ "version": "1.0.0"
+ },
+ "openapi": "3.0.0",
+ "paths": {
+ "/v2/organizations/{slug}/integrations/github/connections": {
+ "get": {
+ "description": "Returns a cursor-paginated list of the GitHub connections of the organization's projects.\n\nUse `page[after]` and `page[before]` to navigate pages and `page[size]` to control the page size.\nPaging walks the organization projects, so a page holds at most `page[size]` connections and can hold fewer (or none) when some of its projects are not connected.\nFollow `links.next` until it is `null` rather than stopping on a short page.\n\nUse `filter[project_ref]` to narrow the list down to a single project.",
+ "operationId": "v2-list-organization-github-connections",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "filter",
+ "required": false,
+ "schema": {
+ "properties": {
+ "project_ref": {
+ "description": "Project ref",
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "style": "deepObject"
+ },
+ {
+ "in": "query",
+ "name": "page",
+ "required": false,
+ "schema": {
+ "properties": {
+ "after": {
+ "description": "Project ref",
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ },
+ "before": {
+ "description": "Project ref",
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ },
+ "size": {
+ "maximum": 100,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "style": "deepObject"
+ },
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListGitHubConnectionsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List GitHub connections of an organization",
+ "tags": ["Organizations"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: projects:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane", "dev-workflows"],
+ "x-fga-permissions": [["organization_projects_read"]],
+ "x-oauth-scope": "projects:read"
+ }
+ },
+ "/v2/organizations/{slug}/members": {
+ "get": {
+ "description": "Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.",
+ "operationId": "v2-list-organization-members",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "filter",
+ "required": false,
+ "schema": {
+ "properties": {
+ "primary_email": {
+ "format": "email",
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "style": "deepObject"
+ },
+ {
+ "in": "query",
+ "name": "page",
+ "required": false,
+ "schema": {
+ "properties": {
+ "after": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "type": "string"
+ },
+ "before": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "type": "string"
+ },
+ "size": {
+ "maximum": 100,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "style": "deepObject"
+ },
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListMembersResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List members of an organization",
+ "tags": ["Organizations"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: organizations:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["members_read"]],
+ "x-oauth-scope": "organizations:read"
+ }
+ },
+ "/v2/organizations/{slug}/members/invitations": {
+ "delete": {
+ "description": "Bulk delete member invitations for an organization by email address.",
+ "operationId": "v2-delete-organization-invitations",
+ "parameters": [
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2DeleteInvitationsRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2DeleteInvitationsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "402": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "This feature requires the Enterprise organization plan."
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Deletes organization invitations by email",
+ "tags": ["Organizations Members Invitations"],
+ "x-allowed-plans": ["Enterprise"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: organizations:write",
+ "position": "after"
+ },
+ {
+ "name": "Only available on Enterprise",
+ "position": "before"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["members_write"]],
+ "x-oauth-scope": "organizations:write"
+ },
+ "post": {
+ "description": "Creates member invitations for an organization. Each invitation can have different role and project scope settings.",
+ "operationId": "v2-create-organization-invitations",
+ "parameters": [
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2CreateInvitationsRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2CreateInvitationsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "402": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "This feature requires the Enterprise organization plan."
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Creates organization invitations",
+ "tags": ["Organizations Members Invitations"],
+ "x-allowed-plans": ["Enterprise"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: organizations:write",
+ "position": "after"
+ },
+ {
+ "name": "Only available on Enterprise",
+ "position": "before"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["members_write"]],
+ "x-oauth-scope": "organizations:write"
+ }
+ },
+ "/v2/organizations/{slug}/members/{user_id}/roles": {
+ "patch": {
+ "description": "Assigns an org-wide role when projects is omitted, or creates a project-scoped assignment when projects is provided. Uses an org-level role template id from GET /v2/organizations/{slug}/roles. Stale role assignments are automatically cleaned up: if a role no longer has any projects, it is deleted; overlapping project assignments in other roles are automatically removed to avoid duplication.",
+ "operationId": "v2-assign-organization-member-role",
+ "parameters": [
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "user_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2AssignOrganizationMemberRoleRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/OrganizationMemberRoleResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "402": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "This feature requires the Enterprise organization plan."
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to assign organization member role"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Assign or change an organization member role",
+ "tags": ["Organizations"],
+ "x-allowed-plans": ["Enterprise"],
+ "x-badges": [
+ {
+ "name": "Only available on Enterprise",
+ "position": "before"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["organization_admin_write"]]
+ }
+ },
+ "/v2/organizations/{slug}/project-creation-rate": {
+ "get": {
+ "description": "Returns the authoritative rate a new project adds to the organization, together with the context that decided it. A zero amount is authoritative and means the project can be created without an additional charge.",
+ "operationId": "v2-get-project-creation-rate",
+ "parameters": [
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ProjectCreationRateResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Organization not found"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Get the rate for creating a project in an organization",
+ "tags": ["Organizations"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: projects:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["billing"],
+ "x-fga-permissions": [["organization_projects_read"]],
+ "x-oauth-scope": "projects:read"
+ }
+ },
+ "/v2/organizations/{slug}/projects": {
+ "get": {
+ "description": "Returns a cursor-paginated list of projects for the specified organization, including their databases.\n\nUse `page[after]` and `page[before]` to navigate pages and `page[size]` to control the number of projects returned per page.",
+ "operationId": "v2-list-organization-projects",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "page",
+ "required": false,
+ "schema": {
+ "properties": {
+ "after": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "before": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "size": {
+ "maximum": 100,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "style": "deepObject"
+ },
+ {
+ "description": "Case-insensitive substring match on the project name.",
+ "in": "query",
+ "name": "search",
+ "required": false,
+ "schema": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Sort order by creation time: `inserted_at` (oldest first) or `-inserted_at` (newest first). Defaults to `inserted_at`.",
+ "in": "query",
+ "name": "sort",
+ "required": false,
+ "schema": {
+ "enum": ["-inserted_at", "inserted_at"],
+ "example": "-inserted_at",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListProjectsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List projects of an organization",
+ "tags": ["Organizations"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: projects:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["organization_projects_read"]],
+ "x-oauth-scope": "projects:read"
+ }
+ },
+ "/v2/organizations/{slug}/roles": {
+ "get": {
+ "description": "Returns a list of org-level roles for the organization.",
+ "operationId": "v2-list-organization-roles",
+ "parameters": [
+ {
+ "description": "Organization slug",
+ "in": "path",
+ "name": "slug",
+ "required": true,
+ "schema": {
+ "example": "tsrqponmlkjihgfedcba",
+ "pattern": "^[\\w-]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListRolesResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List roles of an organization",
+ "tags": ["Organizations"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: organizations:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["members_read"]],
+ "x-oauth-scope": "organizations:read"
+ }
+ },
+ "/v2/projects/{ref}/analytics/log-drains": {
+ "get": {
+ "operationId": "v2-list-log-drains",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ListLogDrainsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to fetch log drains"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List project log drains",
+ "tags": ["Analytics"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: analytics_config:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["analytics"],
+ "x-fga-permissions": [["analytics_config_read"]],
+ "x-oauth-scope": "analytics_config:read"
+ },
+ "post": {
+ "operationId": "v2-create-log-drain",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateLogDrainRequestOpenApi"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LogDrainResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "402": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "This feature requires the Pro, Team, or Enterprise organization plan."
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to create a log drain"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Create a log drain for a project",
+ "tags": ["Analytics"],
+ "x-allowed-plans": ["Enterprise", "Pro", "Team"],
+ "x-badges": [
+ {
+ "name": "Only available on Pro, Team, Enterprise",
+ "position": "before"
+ },
+ {
+ "name": "OAuth scope: analytics_config:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["analytics"],
+ "x-fga-permissions": [["analytics_config_write"]],
+ "x-oauth-scope": "analytics_config:write"
+ }
+ },
+ "/v2/projects/{ref}/analytics/log-drains/{id}": {
+ "delete": {
+ "operationId": "v2-delete-log-drain",
+ "parameters": [
+ {
+ "description": "Log drains identifier",
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to delete a log drain"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Delete a project log drain",
+ "tags": ["Analytics"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: analytics_config:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["analytics"],
+ "x-fga-permissions": [["analytics_config_write"]],
+ "x-oauth-scope": "analytics_config:write"
+ },
+ "put": {
+ "operationId": "v2-update-log-drain",
+ "parameters": [
+ {
+ "description": "Log drains identifier",
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpdateLogDrainRequestOpenApi"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/LogDrainResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to update log drain"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Update a project log drain",
+ "tags": ["Analytics"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: analytics_config:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["analytics"],
+ "x-fga-permissions": [["analytics_config_write"]],
+ "x-oauth-scope": "analytics_config:write"
+ }
+ },
+ "/v2/projects/{ref}/branch-creation-rate": {
+ "get": {
+ "description": "Returns the authoritative rate a new branch of this project adds to its organization, together with the context that decided it. A zero amount is authoritative and means the branch can be created without an additional charge.",
+ "operationId": "v2-get-branch-creation-rate",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2BranchCreationRateResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "404": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Project not found"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Get the rate for creating a branch of a project",
+ "tags": ["Environments"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: environment:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["billing"],
+ "x-fga-permissions": [["branching_development_read"]],
+ "x-oauth-scope": "environment:read"
+ }
+ },
+ "/v2/projects/{ref}/config": {
+ "get": {
+ "description": "Returns the project's database, pooler, Auth, Data API, Realtime and Storage configuration — the same configuration a branch inherits from its base project. Each is the effective config, so a setting the project has never overridden is reported at its platform default rather than as null. Auth secrets are returned as an HMAC of their value. `storage` is read live from the storage service; the rest come from this platform's own records.",
+ "operationId": "v2-get-project-config",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ProjectConfigResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] Get a project's service configuration",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane", "infra"],
+ "x-fga-permissions": [
+ [
+ "auth_config_read",
+ "data_api_config_read",
+ "database_config_read",
+ "database_network_restrictions_read",
+ "database_read",
+ "database_ssl_config_read",
+ "realtime_config_read",
+ "storage_config_read"
+ ]
+ ]
+ }
+ },
+ "/v2/projects/{ref}/private-link/associations": {
+ "get": {
+ "operationId": "v2-list-private-link-associations",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListPrivateLinkAssociationsResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to retrieve AWS accounts for project"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "List AWS accounts attached to the project PrivateLink share",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane", "platform-networking"],
+ "x-fga-permissions": [["project_admin_read"]]
+ },
+ "post": {
+ "description": "Adds an AWS account to the project's PrivateLink configuration and schedules the AWS resources to be created.",
+ "operationId": "v2-create-private-link-association",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2CreatePrivateLinkAssociationRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2PrivateLinkAssociationResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "402": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "This feature requires the Team, or Enterprise organization plan."
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to add AWS account to PrivateLink share"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Add an AWS account to the project PrivateLink share",
+ "tags": ["Projects"],
+ "x-allowed-plans": ["Enterprise", "Team"],
+ "x-badges": [
+ {
+ "name": "Only available on Team, Enterprise",
+ "position": "before"
+ }
+ ],
+ "x-endpoint-owners": ["control-plane", "platform-networking"],
+ "x-fga-permissions": [["project_admin_write"]]
+ }
+ },
+ "/v2/projects/{ref}/private-link/associations/aws-account/{aws_account_id}": {
+ "delete": {
+ "description": "Removes an AWS account from the project's PrivateLink configuration (targeting the primary database). Cleans up the associated AWS resources.",
+ "operationId": "v2-delete-private-link-association",
+ "parameters": [
+ {
+ "description": "AWS account ID used in PrivateLink association",
+ "in": "path",
+ "name": "aws_account_id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to remove AWS account from PrivateLink share"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Remove an AWS account from the project PrivateLink share",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane", "platform-networking"],
+ "x-fga-permissions": [["project_admin_write"]]
+ }
+ },
+ "/v2/projects/{ref}/private-link/associations/aws-account/{aws_account_id}/database/{database_identifier}": {
+ "delete": {
+ "description": "Removes an AWS account from the project's PrivateLink configuration for the given read replica. Cleans up the associated AWS resources.",
+ "operationId": "v2-delete-private-link-association-for-database",
+ "parameters": [
+ {
+ "description": "AWS account ID used in PrivateLink association",
+ "in": "path",
+ "name": "aws_account_id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "Identifier of the read replica this PrivateLink association targets",
+ "in": "path",
+ "name": "database_identifier",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ },
+ "500": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Failed to remove AWS account from PrivateLink share"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Remove an AWS account from a specific database PrivateLink share",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane", "platform-networking"],
+ "x-fga-permissions": [["project_admin_write"]]
+ }
+ },
+ "/v2/projects/{ref}/transfers": {
+ "post": {
+ "operationId": "v2-transfer-a-project",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2TransferProjectBody"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Transfers a project to a different organization",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["organization_admin_write"]]
+ }
+ },
+ "/v2/projects/{ref}/transfers/previews": {
+ "post": {
+ "operationId": "v2-preview-a-project-transfer",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2TransferProjectBody"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2PreviewProjectTransferResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "Previews transferring a project to a different organizations, shows eligibility and impact",
+ "tags": ["Projects"],
+ "x-endpoint-owners": ["control-plane"],
+ "x-fga-permissions": [["project_admin_read"]]
+ }
+ },
+ "/v2/projects/{ref}/workers": {
+ "get": {
+ "description": "Returns all workers you've previously deployed to the specified project.",
+ "operationId": "v2-list-all-workers",
+ "parameters": [
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2ListWorkersResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] List all workers",
+ "tags": ["Workers"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: edge_functions:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["functions"],
+ "x-fga-permissions": [["workers_read"]],
+ "x-oauth-scope": "edge_functions:read"
+ }
+ },
+ "/v2/projects/{ref}/workers/{name}": {
+ "delete": {
+ "description": "Tombstones the worker. Its instances and image are torn down asynchronously.",
+ "operationId": "v2-delete-a-worker",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "name",
+ "required": true,
+ "schema": {
+ "example": "hello-world",
+ "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] Delete a worker",
+ "tags": ["Workers"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: edge_functions:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["functions"],
+ "x-fga-permissions": [["workers_write"]],
+ "x-oauth-scope": "edge_functions:write"
+ },
+ "get": {
+ "description": "Returns a worker along with its instance tally. Poll this after a deploy until `build_state` leaves `building`.",
+ "operationId": "v2-get-a-worker",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "name",
+ "required": true,
+ "schema": {
+ "example": "hello-world",
+ "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2WorkerResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] Retrieve a worker",
+ "tags": ["Workers"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: edge_functions:read",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["functions"],
+ "x-fga-permissions": [["workers_read"]],
+ "x-oauth-scope": "edge_functions:read"
+ }
+ },
+ "/v2/projects/{ref}/workers/{name}/deploy": {
+ "post": {
+ "description": "Creates the worker if it does not exist, building from a context staged through the uploads endpoint. The build runs asynchronously: this answers 202 and the worker reaches `build_state` `active` or `failed` later.",
+ "operationId": "v2-deploy-a-worker",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "name",
+ "required": true,
+ "schema": {
+ "example": "hello-world",
+ "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2DeployWorkerRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "202": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2WorkerResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] Deploy a worker",
+ "tags": ["Workers"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: edge_functions:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["functions"],
+ "x-fga-permissions": [["workers_write"]],
+ "x-oauth-scope": "edge_functions:write"
+ }
+ },
+ "/v2/projects/{ref}/workers/{name}/uploads": {
+ "post": {
+ "description": "PUT the `.tar.gz` build context to the returned `url` before `expires_at`, then deploy with the upload id as `context_upload_id`. The bytes go straight to storage — no management API request carries them.",
+ "operationId": "v2-create-worker-upload",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "name",
+ "required": true,
+ "schema": {
+ "example": "hello-world",
+ "pattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
+ "type": "string"
+ }
+ },
+ {
+ "description": "Project ref",
+ "in": "path",
+ "name": "ref",
+ "required": true,
+ "schema": {
+ "example": "abcdefghijklmnopqrst",
+ "maxLength": 20,
+ "minLength": 20,
+ "pattern": "^[a-z]+$",
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/V2WorkerUploadResponse"
+ }
+ }
+ },
+ "description": ""
+ },
+ "401": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Unauthorized"
+ },
+ "403": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Forbidden action"
+ },
+ "429": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponseBody"
+ }
+ }
+ },
+ "description": "Rate limit exceeded"
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "summary": "[Alpha] Mint a presigned slot for a build-context upload",
+ "tags": ["Workers"],
+ "x-badges": [
+ {
+ "name": "OAuth scope: edge_functions:write",
+ "position": "after"
+ }
+ ],
+ "x-endpoint-owners": ["functions"],
+ "x-fga-permissions": [["workers_write"]],
+ "x-oauth-scope": "edge_functions:write"
+ }
+ }
+ },
+ "servers": [],
+ "tags": []
+}
diff --git a/packages/mcp-server-supabase/src/elicitations.test.ts b/packages/mcp-server-supabase/src/elicitations.test.ts
new file mode 100644
index 00000000..3b52549c
--- /dev/null
+++ b/packages/mcp-server-supabase/src/elicitations.test.ts
@@ -0,0 +1,543 @@
+import {
+ Client,
+ StreamableHTTPClientTransport,
+} from '@modelcontextprotocol/client';
+import type {
+ ClientCapabilities,
+ ElicitResult,
+ ServerContext,
+} from '@modelcontextprotocol/server';
+import { StreamTransport } from '@supabase/mcp-utils';
+import { describe, expect, test } from 'vitest';
+
+import {
+ BRANCH_RATE,
+ createCostPlatform,
+ FIXED_PROJECT,
+ PROJECT_RATE,
+} from '../test/cost-platform.js';
+import {
+ canonicalArgumentsDigest,
+ createSignedStateCodec,
+ createStateSigner,
+} from './elicitations/codec.js';
+import type { ContinuationState } from './elicitations/state.js';
+import type { CreationRate } from './platform/types.js';
+import { COST_CONFIRMATION_POLICY_ID } from './policies/cost-confirmation.js';
+import { createSupabaseMcpServer } from './server.js';
+import { createSupabaseMcpHandler } from './transports/http.js';
+
+const MODERN_PROTOCOL_VERSION = '2026-07-28';
+const MCP_ENDPOINT = new URL('https://mcp.test');
+const STATE_KEY = 'cost-policy-continuation-key-long-enough';
+const ACTOR_ID = 'approver-1';
+
+type SetupOptions = {
+ /** Client capabilities. `{}` is a client that declares nothing. */
+ capabilities?: ClientCapabilities;
+ clientName?: string;
+ optOut?: boolean;
+ readOnly?: boolean;
+ answers?: ElicitResult[];
+ /** Continuation state to attach to every tool call this client makes. */
+ requestState?: string;
+};
+
+/** A modern hosted connection whose serving path can deliver a form. */
+async function setupModern(options: SetupOptions = {}) {
+ const cost = createCostPlatform();
+ const elicited: string[] = [];
+ const handler = createSupabaseMcpHandler({
+ platform: cost.platform,
+ features: ['account', 'branching'],
+ readOnly: options.readOnly,
+ elicitation: {
+ actorId: ACTOR_ID,
+ stateKey: STATE_KEY,
+ formDeliveryAvailable: true,
+ optOut: options.optOut,
+ },
+ });
+
+ const transport = new StreamableHTTPClientTransport(MCP_ENDPOINT, {
+ fetch: async (url, init) => {
+ if (options.requestState === undefined) {
+ return handler.fetch(new Request(url, init));
+ }
+
+ // Stands in for a client redeeming state a previous deployment issued.
+ const body = await new Request(url, init).json();
+ if (body?.method === 'tools/call') {
+ body.params.requestState = options.requestState;
+ }
+ return handler.fetch(
+ new Request(url, { ...init, body: JSON.stringify(body) })
+ );
+ },
+ });
+ const capabilities = options.capabilities ?? { elicitation: {} };
+ const client = new Client(
+ { name: options.clientName ?? 'cost-policy-test-client', version: '1.0.0' },
+ {
+ capabilities,
+ versionNegotiation: { mode: { pin: MODERN_PROTOCOL_VERSION } },
+ }
+ );
+
+ const answers = options.answers ?? [{ action: 'accept' }];
+ // A client that declares no elicitation cannot register a handler for one,
+ // which is the same reason it must never be sent a form.
+ if (capabilities.elicitation !== undefined) {
+ client.setRequestHandler('elicitation/create', async (request) => {
+ const { message } = request.params;
+ elicited.push(typeof message === 'string' ? message : '');
+ const answer = answers.shift();
+ if (answer === undefined) {
+ throw new Error('no elicitation answer left');
+ }
+ return answer;
+ });
+ }
+
+ await client.connect(transport);
+
+ return { client, elicited, ...cost };
+}
+
+/**
+ * A classic hosted connection: the same server options, over a transport that
+ * carries no per-request envelope.
+ */
+async function setupClassic(capabilities: ClientCapabilities) {
+ const cost = createCostPlatform();
+ const clientTransport = new StreamTransport();
+ const serverTransport = new StreamTransport();
+ clientTransport.readable.pipeTo(serverTransport.writable);
+ serverTransport.readable.pipeTo(clientTransport.writable);
+
+ const server = createSupabaseMcpServer({
+ platform: cost.platform,
+ features: ['account', 'branching'],
+ elicitation: {
+ actorId: ACTOR_ID,
+ stateKey: STATE_KEY,
+ formDeliveryAvailable: true,
+ },
+ });
+ const client = new Client(
+ { name: 'classic-client', version: '1.0.0' },
+ { capabilities }
+ );
+ let elicits = 0;
+ client.setRequestHandler('elicitation/create', async () => {
+ elicits += 1;
+ return { action: 'accept' };
+ });
+
+ await server.connect(serverTransport);
+ await client.connect(clientTransport);
+
+ return { client, elicitCount: () => elicits, ...cost };
+}
+
+function textOf(result: { content?: unknown }): string {
+ const content = result.content;
+ if (!Array.isArray(content)) {
+ return '';
+ }
+ return content
+ .map((entry) =>
+ entry !== null &&
+ typeof entry === 'object' &&
+ 'text' in entry &&
+ typeof entry.text === 'string'
+ ? entry.text
+ : ''
+ )
+ .join('');
+}
+
+const PROJECT_ARGS = {
+ name: 'Fixture Project',
+ region: 'us-east-1',
+ organization_id: 'fixed-org',
+};
+
+const BRANCH_ARGS = { project_id: 'fixed-project-ref', name: 'develop' };
+
+/**
+ * State a previous deployment issued: same key, same actor, version 1 of this
+ * policy, and the Boolean the old contract read consent from.
+ */
+async function version1State(args: Record) {
+ const codec = createSignedStateCodec({
+ signer: createStateSigner(STATE_KEY),
+ bind: (ctx) => `${ACTOR_ID}\u0000${ctx.mcpReq.method}`,
+ clock: Date.now,
+ });
+ const issuedAt = Math.floor(Date.now() / 1_000);
+
+ return codec.mint(
+ {
+ v: 1,
+ policy: COST_CONFIRMATION_POLICY_ID,
+ policyVersion: 1,
+ tool: 'create_project',
+ argsDigest: await canonicalArgumentsDigest(args),
+ proposal: { confirm: false },
+ jti: 'version-1-state',
+ iat: issuedAt,
+ exp: issuedAt + 120,
+ },
+ { mcpReq: { method: 'tools/call' } } as unknown as ServerContext
+ );
+}
+
+describe('modern capable creation', () => {
+ test('an accepted project is created with its business output unchanged', async () => {
+ const { client, elicited } = await setupModern();
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(elicited).toHaveLength(1);
+ expect(elicited[0]).toContain(
+ `${PROJECT_RATE.amount} ${PROJECT_RATE.currency}`
+ );
+ expect(result.structuredContent).toStrictEqual(FIXED_PROJECT);
+ // Explicit text beside the unchanged business output: what the client
+ // reported, and what the server did about it.
+ expect(textOf(result)).toContain('The client reported');
+ expect(textOf(result)).toContain('10 USD per month');
+ expect(textOf(result)).toContain('"Fixture Project" was created');
+ });
+
+ test('an accepted branch is created', async () => {
+ const { client, elicited, calls } = await setupModern();
+
+ const result = await client.callTool({
+ name: 'create_branch',
+ arguments: BRANCH_ARGS,
+ });
+
+ expect(elicited[0]).toContain(
+ `${BRANCH_RATE.amount} ${BRANCH_RATE.currency}`
+ );
+ expect(elicited[0]).toContain('per hour');
+ expect(calls).toContain('create_branch');
+ expect(textOf(result)).toContain('The client reported');
+ expect(textOf(result)).toContain('0.01344 USD per hour');
+ expect(textOf(result)).toContain('"develop" was created');
+ expect(result.structuredContent).toMatchObject({ name: 'develop' });
+ });
+
+ test('a declined project reports the client outcome and creates nothing', async () => {
+ const { client, calls } = await setupModern({
+ answers: [{ action: 'decline' }],
+ });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(result.structuredContent).toStrictEqual({ status: 'declined' });
+ expect(textOf(result)).toContain('The client reported');
+ expect(textOf(result)).toContain('no project was created');
+ expect(calls).not.toContain('create_project');
+ });
+
+ test('a cancelled project stays distinct from a declined one', async () => {
+ const { client, calls } = await setupModern({
+ answers: [{ action: 'cancel' }],
+ });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(result.structuredContent).toStrictEqual({ status: 'cancelled' });
+ expect(textOf(result)).toContain('dismissed');
+ expect(calls).not.toContain('create_project');
+ });
+
+ test('a zero authoritative rate creates without asking, and is still guarded', async () => {
+ const { client, calls, elicited, projectRates } = await setupModern();
+ const free: CreationRate = { ...PROJECT_RATE, amount: 0 };
+ projectRates.push(free, free);
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(elicited).toHaveLength(0);
+ expect(result.structuredContent).toStrictEqual(FIXED_PROJECT);
+ // Nothing was asked, so nothing is reported as accepted.
+ expect(textOf(result)).toContain('no confirmation was requested');
+ expect(textOf(result)).not.toContain('client reported');
+ expect(textOf(result)).toContain('was created');
+ expect(calls).toStrictEqual([
+ 'read_project_rate',
+ 'read_project_rate',
+ 'create_project',
+ ]);
+ });
+});
+
+describe('the final authoritative check', () => {
+ test('the rate is read immediately before the creation call', async () => {
+ const { client, calls } = await setupModern();
+
+ await client.callTool({ name: 'create_project', arguments: PROJECT_ARGS });
+
+ // The proposal read, then the read that guards the side effect, then the
+ // side effect. Nothing sits between the last two.
+ expect(calls).toStrictEqual([
+ 'read_project_rate',
+ 'read_project_rate',
+ 'create_project',
+ ]);
+ });
+
+ test('an equal or lower final rate proceeds', async () => {
+ const { client, calls, projectRates } = await setupModern();
+ projectRates.push(PROJECT_RATE, { ...PROJECT_RATE, amount: 4 });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(result.isError).not.toBe(true);
+ expect(calls).toContain('create_project');
+ });
+
+ test.each<[string, CreationRate]>([
+ ['a higher amount', { ...PROJECT_RATE, amount: 11 }],
+ [
+ 'a changed recurrence',
+ { ...PROJECT_RATE, amount: 1, recurrence: 'hourly' },
+ ],
+ ['a changed currency', { ...PROJECT_RATE, amount: 1, currency: 'EUR' }],
+ ])(
+ '%s creates nothing and reports a stale approval',
+ async (_case, final) => {
+ const { client, calls, projectRates } = await setupModern();
+ projectRates.push(PROJECT_RATE, final);
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(result.isError).toBe(true);
+ expect(textOf(result)).toContain('approved_rate_stale');
+ expect(calls).not.toContain('create_project');
+ }
+ );
+
+ test('a branch approval is guarded by the branch rate', async () => {
+ const { client, calls, branchRates } = await setupModern();
+ branchRates.push(BRANCH_RATE, { ...BRANCH_RATE, amount: 0.02 });
+
+ const result = await client.callTool({
+ name: 'create_branch',
+ arguments: BRANCH_ARGS,
+ });
+
+ expect(result.isError).toBe(true);
+ expect(textOf(result)).toContain('approved_rate_stale');
+ expect(calls).not.toContain('create_branch');
+ });
+});
+
+describe('capable discovery and the retired token', () => {
+ test('discovery omits confirm_cost and the legacy token', async () => {
+ const { client } = await setupModern();
+
+ const { tools } = await client.listTools();
+ const createProject = tools.find(
+ (entry) => entry.name === 'create_project'
+ );
+
+ expect(tools.map((entry) => entry.name)).not.toContain('confirm_cost');
+ expect(
+ Object.keys(createProject?.inputSchema.properties ?? {})
+ ).toStrictEqual(['name', 'region', 'organization_id']);
+ expect(createProject?.outputSchema).toBeDefined();
+ });
+
+ test('a direct call to confirm_cost answers with migration guidance', async () => {
+ const { client } = await setupModern();
+
+ const result = await client.callTool({
+ name: 'confirm_cost',
+ arguments: { type: 'project', recurrence: 'monthly', amount: 10 },
+ });
+
+ expect(result.isError).toBe(true);
+ expect(textOf(result)).toContain('create_project');
+ expect(textOf(result)).not.toContain('confirmation_id');
+ });
+
+ test('a supplied legacy token is ignored and cannot bypass the confirmation', async () => {
+ const { client, elicited, calls } = await setupModern({
+ answers: [{ action: 'decline' }],
+ });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: {
+ ...PROJECT_ARGS,
+ confirm_cost_id: 'a-token-from-the-other-lane',
+ },
+ });
+
+ // The token neither bypasses the question nor fails the call: it is
+ // dropped before anything binds to it.
+ expect(elicited).toHaveLength(1);
+ expect(result.structuredContent).toStrictEqual({ status: 'declined' });
+ expect(calls).not.toContain('create_project');
+ });
+
+ test('the flow does not read the client label', async () => {
+ // One contract instead of a compatibility table: a client whose name
+ // matches nothing this package knows still gets the form lane, because the
+ // decision reads capabilities and the serving path only.
+ const { client, elicited } = await setupModern({
+ clientName: 'some-client-nobody-listed',
+ });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(elicited).toHaveLength(1);
+ expect(result.structuredContent).toStrictEqual(FIXED_PROJECT);
+ });
+});
+
+describe('read-only servers', () => {
+ test('a call fails read-only without reading a rate or asking', async () => {
+ const { client, elicited, calls } = await setupModern({ readOnly: true });
+
+ // The tool is policy-free here, so it takes the legacy shape it has
+ // always had in read-only mode, token included.
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: { ...PROJECT_ARGS, confirm_cost_id: 'any-token' },
+ });
+
+ // Policy resolution runs before a tool's own checks, so a policy here
+ // would price and prompt for a creation that cannot happen.
+ expect(result.isError).toBe(true);
+ expect(textOf(result)).toContain('read-only mode');
+ expect(elicited).toHaveLength(0);
+ expect(calls).toStrictEqual([]);
+ });
+});
+
+describe('rolling deployment', () => {
+ test('state issued by policy version 1 creates nothing and says so', async () => {
+ // The one case kept from the Boolean contract, and it is kept as a
+ // version rejection rather than a Boolean one: the old payload carried
+ // `confirm: false`, and this policy refuses it without ever looking at
+ // that content. Version binding itself belongs to the runtime; what is
+ // proved here is the product consequence.
+ const { client, calls } = await setupModern({
+ requestState: await version1State(PROJECT_ARGS),
+ });
+
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: PROJECT_ARGS,
+ });
+
+ expect(result.isError).toBe(true);
+ expect(textOf(result)).toContain('Run the tool again');
+ expect(calls).not.toContain('create_project');
+ expect(result.structuredContent).toBeUndefined();
+ });
+});
+
+describe('surfaces that stay on the legacy contract', () => {
+ test('a modern client that declares no elicitation keeps confirm_cost', async () => {
+ const { client } = await setupModern({ capabilities: {} });
+
+ const { tools } = await client.listTools();
+ const createProject = tools.find(
+ (entry) => entry.name === 'create_project'
+ );
+
+ expect(tools.map((entry) => entry.name)).toContain('confirm_cost');
+ expect(createProject?.inputSchema.required).toContain('confirm_cost_id');
+ expect(createProject?.outputSchema).toBeUndefined();
+ });
+
+ test('a URL-only elicitation declaration is not form support', async () => {
+ const { client } = await setupModern({
+ capabilities: { elicitation: { url: {} } },
+ });
+
+ const { tools } = await client.listTools();
+ expect(tools.map((entry) => entry.name)).toContain('confirm_cost');
+ });
+
+ test('an opted-out connection keeps confirm_cost for a capable client', async () => {
+ const { client } = await setupModern({ optOut: true });
+
+ const { tools } = await client.listTools();
+ const createProject = tools.find(
+ (entry) => entry.name === 'create_project'
+ );
+
+ expect(tools.map((entry) => entry.name)).toContain('confirm_cost');
+ expect(createProject?.inputSchema.required).toContain('confirm_cost_id');
+ expect(createProject?.outputSchema).toBeUndefined();
+ });
+
+ test('a classic client declaring form still gets confirm_cost and no form', async () => {
+ const { client, elicitCount } = await setupClassic({
+ elicitation: { form: {} },
+ });
+
+ const { tools } = await client.listTools();
+ const createProject = tools.find(
+ (entry) => entry.name === 'create_project'
+ );
+
+ expect(tools.map((entry) => entry.name)).toContain('confirm_cost');
+ expect(createProject?.inputSchema.required).toContain('confirm_cost_id');
+ expect(elicitCount()).toBe(0);
+ });
+
+ test('the legacy lane still creates through the confirmation pair', async () => {
+ const { client, calls } = await setupModern({ capabilities: {} });
+
+ const cost = await client.callTool({
+ name: 'get_cost',
+ arguments: { type: 'project', organization_id: 'fixed-org' },
+ });
+ const confirmation = await client.callTool({
+ name: 'confirm_cost',
+ arguments: JSON.parse(textOf(cost)),
+ });
+ const result = await client.callTool({
+ name: 'create_project',
+ arguments: {
+ ...PROJECT_ARGS,
+ confirm_cost_id: JSON.parse(textOf(confirmation)).confirmation_id,
+ },
+ });
+
+ expect(result.isError).not.toBe(true);
+ expect(JSON.parse(textOf(result))).toStrictEqual(FIXED_PROJECT);
+ expect(result.structuredContent).toBeUndefined();
+ expect(calls).toContain('create_project');
+ });
+});
diff --git a/packages/mcp-server-supabase/src/index.ts b/packages/mcp-server-supabase/src/index.ts
index bc20ab46..ae568a0e 100644
--- a/packages/mcp-server-supabase/src/index.ts
+++ b/packages/mcp-server-supabase/src/index.ts
@@ -4,6 +4,7 @@ export type { ToolCallCallback } from '@supabase/mcp-utils';
export type { SupabasePlatform } from './platform/index.js';
export {
createSupabaseMcpServer,
+ type SupabaseElicitationOptions,
type SupabaseMcpServerOptions,
} from './server.js';
export { createSupabaseMcpHandler } from './transports/http.js';
diff --git a/packages/mcp-server-supabase/src/management-api/index.ts b/packages/mcp-server-supabase/src/management-api/index.ts
index 74f568b3..f928a9fe 100644
--- a/packages/mcp-server-supabase/src/management-api/index.ts
+++ b/packages/mcp-server-supabase/src/management-api/index.ts
@@ -9,14 +9,25 @@ import type {
SuccessResponse,
} from 'openapi-typescript-helpers';
import { z } from 'zod/v4';
-import type { paths } from './types.js';
+import type { paths as v1Paths } from './types.js';
+import type { paths as v2Paths } from './v2-types.js';
+
+/**
+ * Every Management API version this package speaks, in one path map.
+ *
+ * The versions are separate generated modules with disjoint path prefixes, so
+ * one client covers both and a call site names the version in the path it
+ * requests. New public endpoints start at v2 while every existing caller keeps
+ * the v1 contract it was written against.
+ */
+export type ManagementApiPaths = v1Paths & v2Paths;
export function createManagementApiClient(
baseUrl: string,
accessToken: string,
headers: Record = {}
) {
- return createClient({
+ return createClient({
baseUrl,
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -25,7 +36,7 @@ export function createManagementApiClient(
});
}
-export type ManagementApiClient = Client;
+export type ManagementApiClient = Client;
export type SuccessResponseType<
T extends Record,
diff --git a/packages/mcp-server-supabase/src/management-api/types.ts b/packages/mcp-server-supabase/src/management-api/types.ts
index f675abf1..0f614970 100644
--- a/packages/mcp-server-supabase/src/management-api/types.ts
+++ b/packages/mcp-server-supabase/src/management-api/types.ts
@@ -1284,7 +1284,7 @@ export interface paths {
* If both are not provided, only the last 1 minute of logs will be queried.
* The timestamp range must be no more than 24 hours and is rounded to the nearest minute. If the range is more than 24 hours, a validation error will be thrown.
*
- * Note: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer:~:text=logs%20from%20the-,Sources,-drop%2Ddown%3A) for all available sources.
+ * Note: Unless the `sql` parameter is provided, only edge_logs will be queried. See the [log query docs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#logs-explorer) for all available sources.
*
*/
get: operations["v1-get-project-logs-all"];
@@ -9798,7 +9798,7 @@ export interface operations {
"v1-get-project-logs-all": {
parameters: {
query?: {
- /** @description Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details. */
+ /** @description Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details. */
sql?: string;
iso_timestamp_start?: string;
iso_timestamp_end?: string;
@@ -9853,7 +9853,7 @@ export interface operations {
"v1-get-project-logs": {
parameters: {
query?: {
- /** @description Custom SQL query to execute on the logs. See [querying logs](/docs/guides/telemetry/logs?queryGroups=product&product=postgres&queryGroups=source&source=edge_logs#querying-with-the-logs-explorer) for more details. */
+ /** @description Custom SQL query to execute on the logs. See [querying logs](https://supabase.com/docs/guides/monitoring-and-debugging/logs#querying-with-the-logs-explorer) for more details. */
sql?: string;
iso_timestamp_start?: string;
iso_timestamp_end?: string;
diff --git a/packages/mcp-server-supabase/src/management-api/v2-types.ts b/packages/mcp-server-supabase/src/management-api/v2-types.ts
new file mode 100644
index 00000000..46703c1b
--- /dev/null
+++ b/packages/mcp-server-supabase/src/management-api/v2-types.ts
@@ -0,0 +1,3183 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ '/v2/organizations/{slug}/integrations/github/connections': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List GitHub connections of an organization
+ * @description Returns a cursor-paginated list of the GitHub connections of the organization's projects.
+ *
+ * Use `page[after]` and `page[before]` to navigate pages and `page[size]` to control the page size.
+ * Paging walks the organization projects, so a page holds at most `page[size]` connections and can hold fewer (or none) when some of its projects are not connected.
+ * Follow `links.next` until it is `null` rather than stopping on a short page.
+ *
+ * Use `filter[project_ref]` to narrow the list down to a single project.
+ */
+ get: operations['v2-list-organization-github-connections'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/members': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List members of an organization
+ * @description Returns a cursor-paginated list of organization members including their roles and project-scoped permissions.
+ */
+ get: operations['v2-list-organization-members'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/members/invitations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * Creates organization invitations
+ * @description Creates member invitations for an organization. Each invitation can have different role and project scope settings.
+ */
+ post: operations['v2-create-organization-invitations'];
+ /**
+ * Deletes organization invitations by email
+ * @description Bulk delete member invitations for an organization by email address.
+ */
+ delete: operations['v2-delete-organization-invitations'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/members/{user_id}/roles': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ /**
+ * Assign or change an organization member role
+ * @description Assigns an org-wide role when projects is omitted, or creates a project-scoped assignment when projects is provided. Uses an org-level role template id from GET /v2/organizations/{slug}/roles. Stale role assignments are automatically cleaned up: if a role no longer has any projects, it is deleted; overlapping project assignments in other roles are automatically removed to avoid duplication.
+ */
+ patch: operations['v2-assign-organization-member-role'];
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/project-creation-rate': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get the rate for creating a project in an organization
+ * @description Returns the authoritative rate a new project adds to the organization, together with the context that decided it. A zero amount is authoritative and means the project can be created without an additional charge.
+ */
+ get: operations['v2-get-project-creation-rate'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/projects': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List projects of an organization
+ * @description Returns a cursor-paginated list of projects for the specified organization, including their databases.
+ *
+ * Use `page[after]` and `page[before]` to navigate pages and `page[size]` to control the number of projects returned per page.
+ */
+ get: operations['v2-list-organization-projects'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/organizations/{slug}/roles': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * List roles of an organization
+ * @description Returns a list of org-level roles for the organization.
+ */
+ get: operations['v2-list-organization-roles'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/analytics/log-drains': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List project log drains */
+ get: operations['v2-list-log-drains'];
+ put?: never;
+ /** Create a log drain for a project */
+ post: operations['v2-create-log-drain'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/analytics/log-drains/{id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ /** Update a project log drain */
+ put: operations['v2-update-log-drain'];
+ post?: never;
+ /** Delete a project log drain */
+ delete: operations['v2-delete-log-drain'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/branch-creation-rate': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get the rate for creating a branch of a project
+ * @description Returns the authoritative rate a new branch of this project adds to its organization, together with the context that decided it. A zero amount is authoritative and means the branch can be created without an additional charge.
+ */
+ get: operations['v2-get-branch-creation-rate'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/config': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * [Alpha] Get a project's service configuration
+ * @description Returns the project's database, pooler, Auth, Data API, Realtime and Storage configuration — the same configuration a branch inherits from its base project. Each is the effective config, so a setting the project has never overridden is reported at its platform default rather than as null. Auth secrets are returned as an HMAC of their value. `storage` is read live from the storage service; the rest come from this platform's own records.
+ */
+ get: operations['v2-get-project-config'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/private-link/associations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List AWS accounts attached to the project PrivateLink share */
+ get: operations['v2-list-private-link-associations'];
+ put?: never;
+ /**
+ * Add an AWS account to the project PrivateLink share
+ * @description Adds an AWS account to the project's PrivateLink configuration and schedules the AWS resources to be created.
+ */
+ post: operations['v2-create-private-link-association'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/private-link/associations/aws-account/{aws_account_id}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /**
+ * Remove an AWS account from the project PrivateLink share
+ * @description Removes an AWS account from the project's PrivateLink configuration (targeting the primary database). Cleans up the associated AWS resources.
+ */
+ delete: operations['v2-delete-private-link-association'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/private-link/associations/aws-account/{aws_account_id}/database/{database_identifier}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /**
+ * Remove an AWS account from a specific database PrivateLink share
+ * @description Removes an AWS account from the project's PrivateLink configuration for the given read replica. Cleans up the associated AWS resources.
+ */
+ delete: operations['v2-delete-private-link-association-for-database'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/transfers': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Transfers a project to a different organization */
+ post: operations['v2-transfer-a-project'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/transfers/previews': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Previews transferring a project to a different organizations, shows eligibility and impact */
+ post: operations['v2-preview-a-project-transfer'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/workers': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * [Alpha] List all workers
+ * @description Returns all workers you've previously deployed to the specified project.
+ */
+ get: operations['v2-list-all-workers'];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/workers/{name}': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * [Alpha] Retrieve a worker
+ * @description Returns a worker along with its instance tally. Poll this after a deploy until `build_state` leaves `building`.
+ */
+ get: operations['v2-get-a-worker'];
+ put?: never;
+ post?: never;
+ /**
+ * [Alpha] Delete a worker
+ * @description Tombstones the worker. Its instances and image are torn down asynchronously.
+ */
+ delete: operations['v2-delete-a-worker'];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/workers/{name}/deploy': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * [Alpha] Deploy a worker
+ * @description Creates the worker if it does not exist, building from a context staged through the uploads endpoint. The build runs asynchronously: this answers 202 and the worker reaches `build_state` `active` or `failed` later.
+ */
+ post: operations['v2-deploy-a-worker'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ '/v2/projects/{ref}/workers/{name}/uploads': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /**
+ * [Alpha] Mint a presigned slot for a build-context upload
+ * @description PUT the `.tar.gz` build context to the returned `url` before `expires_at`, then deploy with the upload id as `context_upload_id`. The bytes go straight to storage — no management API request carries them.
+ */
+ post: operations['v2-create-worker-upload'];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ CreateLogDrainRequestOpenApi: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ backend_type:
+ | 'axiom'
+ | 'bigquery'
+ | 'clickhouse'
+ | 'datadog'
+ | 'last9'
+ | 'loki'
+ | 'otlp'
+ | 'postgres'
+ | 's3'
+ | 'sentry'
+ | 'syslog'
+ | 'webhook';
+ config:
+ | {
+ hostname?: string;
+ password?: string | null;
+ port?: number | null;
+ schema?: string;
+ url?: string | null;
+ username?: string | null;
+ }
+ | {
+ gzip?: boolean;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ http?: 'http1' | 'http2';
+ url?: string;
+ }
+ | {
+ dataset_id?: string;
+ project_id?: string;
+ }
+ | {
+ api_key?: string;
+ region?: string;
+ }
+ | {
+ headers?: {
+ [key: string]: string;
+ };
+ password?: string | null;
+ url?: string;
+ username?: string | null;
+ }
+ | {
+ dsn?: string;
+ }
+ | {
+ api_token?: string;
+ dataset_name?: string;
+ domain?: string;
+ }
+ | {
+ ca_cert?: string;
+ cipher_key?: string;
+ client_cert?: string;
+ client_key?: string;
+ host?: string;
+ port?: number;
+ structured_data?: string;
+ /** @default false */
+ tls: boolean;
+ };
+ description?: string;
+ name: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'log_drain';
+ };
+ };
+ ErrorResponseBody: {
+ error: components['schemas']['ErrorResponseBodyAPIErrorObject'];
+ };
+ ErrorResponseBodyAPIErrorObject: {
+ code: string;
+ description?: string;
+ id?: string;
+ issues?: components['schemas']['ErrorResponseBodyAPIErrorObject'][];
+ links?: {
+ [key: string]: {
+ describedby?: string;
+ href: string;
+ meta?: {
+ [key: string]: unknown;
+ };
+ rel?: string;
+ title?: string;
+ type?: string;
+ };
+ };
+ message: string;
+ meta?: {
+ [key: string]: unknown;
+ };
+ };
+ ListLogDrainsResponse: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ backend_type:
+ | 'axiom'
+ | 'bigquery'
+ | 'clickhouse'
+ | 'datadog'
+ | 'last9'
+ | 'loki'
+ | 'otlp'
+ | 'postgres'
+ | 's3'
+ | 'sentry'
+ | 'syslog'
+ | 'webhook';
+ config:
+ | {
+ hostname?: string;
+ password?: string | null;
+ port?: number | null;
+ schema?: string;
+ url?: string | null;
+ username?: string | null;
+ }
+ | {
+ gzip?: boolean;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ http?: 'http1' | 'http2';
+ url?: string;
+ }
+ | {
+ dataset_id?: string;
+ project_id?: string;
+ }
+ | {
+ api_key?: string;
+ region?: string;
+ }
+ | {
+ headers?: {
+ [key: string]: string;
+ };
+ password?: string | null;
+ url?: string;
+ username?: string | null;
+ }
+ | {
+ dsn?: string;
+ }
+ | {
+ api_token?: string;
+ dataset_name?: string;
+ domain?: string;
+ }
+ | {
+ ca_cert?: string;
+ cipher_key?: string;
+ client_cert?: string;
+ client_key?: string;
+ host?: string;
+ port?: number;
+ structured_data?: string;
+ /** @default false */
+ tls: boolean;
+ };
+ description?: string;
+ name: string;
+ };
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'log_drain';
+ }[];
+ };
+ LogDrainResponse: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ backend_type:
+ | 'axiom'
+ | 'bigquery'
+ | 'clickhouse'
+ | 'datadog'
+ | 'last9'
+ | 'loki'
+ | 'otlp'
+ | 'postgres'
+ | 's3'
+ | 'sentry'
+ | 'syslog'
+ | 'webhook';
+ config:
+ | {
+ hostname?: string;
+ password?: string | null;
+ port?: number | null;
+ schema?: string;
+ url?: string | null;
+ username?: string | null;
+ }
+ | {
+ gzip?: boolean;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ http?: 'http1' | 'http2';
+ url?: string;
+ }
+ | {
+ dataset_id?: string;
+ project_id?: string;
+ }
+ | {
+ api_key?: string;
+ region?: string;
+ }
+ | {
+ headers?: {
+ [key: string]: string;
+ };
+ password?: string | null;
+ url?: string;
+ username?: string | null;
+ }
+ | {
+ dsn?: string;
+ }
+ | {
+ api_token?: string;
+ dataset_name?: string;
+ domain?: string;
+ }
+ | {
+ ca_cert?: string;
+ cipher_key?: string;
+ client_cert?: string;
+ client_key?: string;
+ host?: string;
+ port?: number;
+ structured_data?: string;
+ /** @default false */
+ tls: boolean;
+ };
+ description?: string;
+ name: string;
+ };
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'log_drain';
+ };
+ };
+ OrganizationMemberRoleResponse: {
+ data: {
+ attributes: {
+ /**
+ * @description Role name. For project-scoped assignments this is the base role name.
+ * @example developer
+ */
+ name: string;
+ /** @description Project refs this role is scoped to. Empty array for org-level roles. */
+ projects: {
+ name: string;
+ ref: string;
+ }[];
+ /**
+ * @description Whether this role applies org-wide or is scoped to specific projects for the user.
+ * @enum {string}
+ */
+ scope: 'organization' | 'project';
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_member_role';
+ };
+ };
+ UpdateLogDrainRequestOpenApi: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ backend_type:
+ | 'axiom'
+ | 'bigquery'
+ | 'clickhouse'
+ | 'datadog'
+ | 'last9'
+ | 'loki'
+ | 'otlp'
+ | 'postgres'
+ | 's3'
+ | 'sentry'
+ | 'syslog'
+ | 'webhook';
+ config?:
+ | {
+ hostname?: string;
+ password?: string | null;
+ port?: number | null;
+ schema?: string;
+ url?: string | null;
+ username?: string | null;
+ }
+ | {
+ gzip?: boolean;
+ headers?: {
+ [key: string]: string;
+ };
+ /** @enum {string} */
+ http?: 'http1' | 'http2';
+ url?: string;
+ }
+ | {
+ dataset_id?: string;
+ project_id?: string;
+ }
+ | {
+ api_key?: string;
+ region?: string;
+ }
+ | {
+ headers?: {
+ [key: string]: string;
+ };
+ password?: string | null;
+ url?: string;
+ username?: string | null;
+ }
+ | {
+ dsn?: string;
+ }
+ | {
+ api_token?: string;
+ dataset_name?: string;
+ domain?: string;
+ }
+ | {
+ ca_cert?: string;
+ cipher_key?: string;
+ client_cert?: string;
+ client_key?: string;
+ host?: string;
+ port?: number;
+ structured_data?: string;
+ /** @default false */
+ tls: boolean;
+ };
+ description?: string;
+ name?: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'log_drain';
+ };
+ };
+ V2AssignOrganizationMemberRoleRequest: {
+ data: {
+ attributes: {
+ /** @description The projects to assign a project-scoped role for. If omitted, assigns an org-wide role. */
+ projects?: {
+ /**
+ * @description Project ref
+ * @example abcjuqabhgwjjutfvtpa
+ */
+ ref: string;
+ }[];
+ /**
+ * @description Role name to assign. Must be one of: owner, administrator, developer, read-only. Must be on a Team or Enterprise plan to use the read-only role.
+ * @example developer
+ * @enum {string}
+ */
+ role: 'administrator' | 'developer' | 'owner' | 'read-only';
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_member_role';
+ };
+ };
+ V2BranchCreationRateResponse: {
+ data: {
+ attributes: {
+ /**
+ * @description Authoritative rate a new branch adds to the organization. Zero is authoritative and means the branch can be created without an additional charge.
+ * @example 0.01344
+ */
+ amount: number;
+ /**
+ * @description ISO 4217 currency of the amount.
+ * @example USD
+ */
+ currency: string;
+ /**
+ * @description Organization the charge lands on.
+ * @example my-org
+ */
+ organization_slug: string;
+ /**
+ * @description Project the branch would be created under.
+ * @example abcdefghijklmnopqrst
+ */
+ parent_project_ref: string;
+ /**
+ * @description Plan that decided the amount.
+ * @example pro
+ */
+ plan_id: string;
+ /**
+ * @description Interval the amount recurs at.
+ * @example hourly
+ * @enum {string}
+ */
+ recurrence: 'hourly' | 'monthly';
+ } & {
+ [key: string]: unknown;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'branch_creation_rate';
+ };
+ };
+ V2CreateInvitationsRequest: {
+ data: {
+ attributes: {
+ /** Format: email */
+ email: string;
+ /** @description The projects to limit a user to. If omitted, user will have org-wide access with the provided role. */
+ projects?: {
+ /**
+ * @description Project ref
+ * @example abcjuqabhgwjjutfvtpa
+ */
+ ref: string;
+ }[];
+ require_sso?: boolean;
+ /**
+ * @description Role name to assign. Must be on a Team or Enterprise plan to use the read-only role.
+ * @example developer
+ * @enum {string}
+ */
+ role: 'administrator' | 'developer' | 'owner' | 'read-only';
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_invitation';
+ }[];
+ };
+ V2CreateInvitationsResponse: {
+ data: {
+ attributes: {
+ /** Format: email */
+ email: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_invitation';
+ }[];
+ error?: {
+ code: string;
+ description?: string;
+ id?: string;
+ issues?: {
+ code: string;
+ description?: string;
+ id?: string;
+ links?: {
+ [key: string]: {
+ describedby?: string;
+ href: string;
+ meta?: {
+ [key: string]: unknown;
+ };
+ rel?: string;
+ title?: string;
+ type?: string;
+ };
+ };
+ message: string;
+ meta: {
+ /** Format: email */
+ email: string;
+ };
+ }[];
+ links?: {
+ [key: string]: {
+ describedby?: string;
+ href: string;
+ meta?: {
+ [key: string]: unknown;
+ };
+ rel?: string;
+ title?: string;
+ type?: string;
+ };
+ };
+ message: string;
+ meta?: {
+ [key: string]: unknown;
+ };
+ };
+ };
+ V2CreatePrivateLinkAssociationRequest: {
+ data: {
+ attributes: {
+ /** @description Optional human-readable name for the AWS account. */
+ account_name?: string;
+ /** @description The AWS account ID to add to the project PrivateLink share. */
+ aws_account_id: string;
+ /** @description Identifier of the read replica this PrivateLink share should target. Omit to target the primary database. */
+ database_identifier?: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'private_link_association';
+ };
+ };
+ V2DeleteInvitationsRequest: {
+ data: {
+ attributes: {
+ /** Format: email */
+ email: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_invitation';
+ }[];
+ };
+ V2DeleteInvitationsResponse: {
+ data: {
+ attributes: {
+ /** Format: email */
+ email: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_invitation';
+ }[];
+ };
+ V2DeployWorkerRequest: {
+ data: {
+ attributes: {
+ /** @description Id of a build context staged through the uploads endpoint. Required unless `runtime` is set. */
+ context_upload_id?: string;
+ spec: {
+ /** @example public */
+ exposure: string;
+ /** @example 1 */
+ instances: number;
+ /** @example node */
+ runtime?: string;
+ /** @example 2gb-1vcpu */
+ size: string;
+ };
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_worker';
+ };
+ };
+ V2ListGitHubConnectionsResponse: {
+ data: {
+ attributes: {
+ /** @description Maximum number of preview branches */
+ branch_limit: number;
+ /** @description When the connection was created */
+ inserted_at: string;
+ /** @description GitHub App installation id */
+ installation_id: number;
+ /** @description Whether a preview branch is created for every pull request */
+ new_branch_per_pr: boolean;
+ /** @description The connected Supabase project */
+ project: {
+ id: number;
+ name: string;
+ /**
+ * @description Project ref
+ * @example abcdefghijklmnopqrst
+ */
+ ref: string;
+ };
+ /** @description The connected GitHub repository */
+ repository: {
+ id: number;
+ name: string;
+ };
+ /** @description Whether branches are only created for changes under `supabase/` */
+ supabase_changes_only: boolean;
+ /** @description When the connection was last updated */
+ updated_at: string;
+ /** @description The user who created the connection, if still known */
+ user: {
+ id: number;
+ primary_email: string | null;
+ username: string;
+ } | null;
+ /** @description Directory within the repository the project lives in */
+ workdir: string;
+ };
+ /**
+ * @description Connection id.
+ * @example 7
+ */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'github_connection';
+ }[];
+ links: {
+ /**
+ * @description URL path to the first page if available.
+ * @example /v2/organizations/my-org/integrations/github/connections?page[size]=10
+ */
+ first?: string | null;
+ /**
+ * @description URL path to the last page if available.
+ * @example /v2/organizations/my-org/integrations/github/connections?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295
+ */
+ last?: string | null;
+ /**
+ * @description URL path to the next page.
+ * @example /v2/organizations/my-org/integrations/github/connections?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4
+ */
+ next: string | null;
+ /**
+ * @description URL path to the previous page.
+ * @example /v2/organizations/my-org/integrations/github/connections?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7
+ */
+ prev: string | null;
+ };
+ };
+ V2ListMembersResponse: {
+ data: {
+ attributes: {
+ /** @description Member's avatar URL */
+ avatar_url: string | null;
+ /** @description Whether this member is a Single Sign-On user */
+ is_sso_user: boolean;
+ /** @description Whether Multi-Factor Authentication is enabled for this member */
+ mfa_enabled: boolean;
+ /** @description Member's primary email */
+ primary_email: string | null;
+ /** @description Roles assigned to this member. Includes both org-level and project-scoped roles. */
+ roles: {
+ /**
+ * @description Role name. For project-scoped roles this is the base role name.
+ * @example developer
+ */
+ name: string;
+ /** @description Project refs this role is scoped to. Empty array for org-level roles. */
+ projects: {
+ name: string;
+ ref: string;
+ }[];
+ /**
+ * @description Whether this role applies org-wide or is scoped to specific projects for the user.
+ * @enum {string}
+ */
+ scope: 'organization' | 'project';
+ }[];
+ /** @description Member's username */
+ username: string | null;
+ };
+ /** Format: uuid */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_member';
+ }[];
+ links: {
+ /**
+ * @description URL path to the first page if available.
+ * @example /v2/organizations/my-org/members?page[size]=10
+ */
+ first?: string | null;
+ /**
+ * @description URL path to the last page if available.
+ * @example /v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295
+ */
+ last?: string | null;
+ /**
+ * @description URL path to the next page.
+ * @example /v2/organizations/my-org/members?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4
+ */
+ next: string | null;
+ /**
+ * @description URL path to the previous page.
+ * @example /v2/organizations/my-org/members?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7
+ */
+ prev: string | null;
+ };
+ };
+ V2ListPrivateLinkAssociationsResponse: {
+ data: {
+ attributes: {
+ /** @description Human-readable name for the AWS account. */
+ account_name?: string;
+ /** @description The AWS account ID this PrivateLink share is associated with. */
+ aws_account_id: string;
+ /** @description Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier. */
+ database_identifier: string;
+ /**
+ * @description Whether this PrivateLink share targets the primary database or a read replica.
+ * @enum {string}
+ */
+ database_type: 'PRIMARY' | 'READ_REPLICA';
+ /** @description ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share. */
+ resource_access_manager_resource_config_arn?: string;
+ /** @description ID of the AWS VPC Lattice resource configuration backing this PrivateLink share. */
+ resource_access_manager_resource_config_id?: string;
+ /** @description ARN of the AWS Resource Access Manager resource share for this association. */
+ resource_access_manager_share_arn?: string;
+ /**
+ * Format: date-time
+ * @description The time and date at which the AWS Resource Share Association was requested from Supabase. `null` means that the association was not yet requested while the PrivateLink Association is pending.
+ */
+ shared_at: string | null;
+ /**
+ * @description
+ * - `CREATING`: The PrivateLink resources and the Association are in the process of being created. The PrivateLink Share cannot be accepted yet.
+ * - `READY`: The PrivateLink resources have been created and the PrivateLink Share can be accepted for the duration of 12h after sharing. See `shared_at`.
+ * - `ASSOCIATION_REQUEST_EXPIRED`: The PrivateLink Share has not been accepted within the 12h time limit. This association can now be deleted.
+ * - `ASSOCIATION_ACCEPTED`: The PrivateLink Share was successfully accepted.
+ * - `CREATION_FAILED`: The PrivateLink resources failed to create. This likely means something went wrong on Supabase's and and support should be contacted.
+ * - `DELETING`: The PrivateLink resources and the Association are in the process of being deleted. The PrivateLink Share cannot be accepted yet.
+ *
+ * @enum {string}
+ */
+ status:
+ | 'ASSOCIATION_ACCEPTED'
+ | 'ASSOCIATION_REQUEST_EXPIRED'
+ | 'CREATING'
+ | 'CREATION_FAILED'
+ | 'DELETING'
+ | 'READY';
+ };
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'private_link_association';
+ }[];
+ };
+ V2ListProjectsResponse: {
+ data: {
+ attributes: {
+ /** @description Cloud provider hosting the project */
+ cloud_provider: string;
+ /** @description The project's databases including compute and disk attributes. */
+ databases: {
+ cloud_provider: string;
+ disk_last_modified_at?: string;
+ disk_throughput_mbps?: number;
+ /** @enum {string} */
+ disk_type?: 'gp3' | 'io2';
+ disk_volume_size_gb?: number;
+ identifier: string;
+ /** @enum {string} */
+ infra_compute_size?:
+ | '12xlarge'
+ | '16xlarge'
+ | '24xlarge'
+ | '24xlarge_high_memory'
+ | '24xlarge_optimized_cpu'
+ | '24xlarge_optimized_memory'
+ | '2xlarge'
+ | '48xlarge'
+ | '48xlarge_high_memory'
+ | '48xlarge_optimized_cpu'
+ | '48xlarge_optimized_memory'
+ | '4xlarge'
+ | '8xlarge'
+ | 'large'
+ | 'medium'
+ | 'micro'
+ | 'nano'
+ | 'pico'
+ | 'small'
+ | 'xlarge';
+ region: string | null;
+ /** @enum {string} */
+ status:
+ | 'ACTIVE_HEALTHY'
+ | 'ACTIVE_UNHEALTHY'
+ | 'COMING_UP'
+ | 'GOING_DOWN'
+ | 'INIT_FAILED'
+ | 'INIT_READ_REPLICA'
+ | 'INIT_READ_REPLICA_FAILED'
+ | 'REMOVED'
+ | 'RESIZING'
+ | 'RESTARTING'
+ | 'RESTORING'
+ | 'UNKNOWN';
+ /** @enum {string} */
+ type: 'PRIMARY' | 'READ_REPLICA';
+ }[];
+ /** @description When the project was created */
+ inserted_at: string;
+ /** @description Project name */
+ name: string;
+ /** @description Region the project is hosted in */
+ region: string;
+ /**
+ * @description Project status
+ * @enum {string}
+ */
+ status:
+ | 'ACTIVE_HEALTHY'
+ | 'ACTIVE_UNHEALTHY'
+ | 'COMING_UP'
+ | 'GOING_DOWN'
+ | 'INACTIVE'
+ | 'INIT_FAILED'
+ | 'PAUSE_FAILED'
+ | 'PAUSING'
+ | 'REMOVED'
+ | 'RESIZING'
+ | 'RESTARTING'
+ | 'RESTORE_FAILED'
+ | 'RESTORING'
+ | 'UNKNOWN'
+ | 'UPGRADING';
+ };
+ /**
+ * @description Project ref
+ * @example abcdefghijklmnopqrst
+ */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project';
+ }[];
+ links: {
+ /**
+ * @description URL path to the first page if available.
+ * @example /v2/organizations/my-org/projects?page[size]=10
+ */
+ first?: string | null;
+ /**
+ * @description URL path to the last page if available.
+ * @example /v2/organizations/my-org/projects?page[size]=10&page[after]=019adf7d-4513-71ba-b264-21900edb4295
+ */
+ last?: string | null;
+ /**
+ * @description URL path to the next page.
+ * @example /v2/organizations/my-org/projects?page[size]=10&page[after]=019adf7d-4513-7062-b292-78b86cc470a4
+ */
+ next: string | null;
+ /**
+ * @description URL path to the previous page.
+ * @example /v2/organizations/my-org/projects?page[size]=10&page[before]=019adf7d-4513-74c5-bb9a-f1bc0f7a95d7
+ */
+ prev: string | null;
+ };
+ };
+ V2ListRolesResponse: {
+ data: {
+ attributes: {
+ /**
+ * @description Role name.
+ * @example developer
+ */
+ name: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'organization_role';
+ }[];
+ };
+ V2ListWorkersResponse: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ build_state: 'active' | 'building' | 'failed';
+ deleting?: boolean;
+ image_version?: string;
+ instances?: {
+ declared: number;
+ live: number;
+ ready: number;
+ stale: number;
+ };
+ instances_error?: string;
+ secret_generation: string;
+ spec: {
+ /** @example public */
+ exposure: string;
+ /** @example 1 */
+ instances: number;
+ /** @example node */
+ runtime?: string;
+ /** @example 2gb-1vcpu */
+ size: string;
+ };
+ state_reason?: string;
+ };
+ /**
+ * @description Worker name.
+ * @example hello-world
+ */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_worker';
+ }[];
+ };
+ V2PreviewProjectTransferResponse: {
+ data: {
+ attributes: {
+ errors: {
+ key: string;
+ message: string;
+ }[];
+ info: {
+ key: string;
+ message: string;
+ }[];
+ valid: boolean;
+ warnings: {
+ key: string;
+ message: string;
+ }[];
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_transfer_result';
+ };
+ };
+ V2PrivateLinkAssociationResponse: {
+ data: {
+ attributes: {
+ /** @description Human-readable name for the AWS account. */
+ account_name?: string;
+ /** @description The AWS account ID this PrivateLink share is associated with. */
+ aws_account_id: string;
+ /** @description Identifier of the database this PrivateLink share targets - the project ref for the primary, or the read replica identifier. */
+ database_identifier: string;
+ /**
+ * @description Whether this PrivateLink share targets the primary database or a read replica.
+ * @enum {string}
+ */
+ database_type: 'PRIMARY' | 'READ_REPLICA';
+ /** @description ARN of the AWS VPC Lattice resource configuration backing this PrivateLink share. */
+ resource_access_manager_resource_config_arn?: string;
+ /** @description ID of the AWS VPC Lattice resource configuration backing this PrivateLink share. */
+ resource_access_manager_resource_config_id?: string;
+ /** @description ARN of the AWS Resource Access Manager resource share for this association. */
+ resource_access_manager_share_arn?: string;
+ /**
+ * Format: date-time
+ * @description The time and date at which the AWS Resource Share Association was requested from Supabase. `null` means that the association was not yet requested while the PrivateLink Association is pending.
+ */
+ shared_at: string | null;
+ /**
+ * @description
+ * - `CREATING`: The PrivateLink resources and the Association are in the process of being created. The PrivateLink Share cannot be accepted yet.
+ * - `READY`: The PrivateLink resources have been created and the PrivateLink Share can be accepted for the duration of 12h after sharing. See `shared_at`.
+ * - `ASSOCIATION_REQUEST_EXPIRED`: The PrivateLink Share has not been accepted within the 12h time limit. This association can now be deleted.
+ * - `ASSOCIATION_ACCEPTED`: The PrivateLink Share was successfully accepted.
+ * - `CREATION_FAILED`: The PrivateLink resources failed to create. This likely means something went wrong on Supabase's and and support should be contacted.
+ * - `DELETING`: The PrivateLink resources and the Association are in the process of being deleted. The PrivateLink Share cannot be accepted yet.
+ *
+ * @enum {string}
+ */
+ status:
+ | 'ASSOCIATION_ACCEPTED'
+ | 'ASSOCIATION_REQUEST_EXPIRED'
+ | 'CREATING'
+ | 'CREATION_FAILED'
+ | 'DELETING'
+ | 'READY';
+ };
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'private_link_association';
+ };
+ };
+ V2ProjectConfigResponse: {
+ data: {
+ attributes: {
+ api: {
+ db_extra_search_path: string;
+ /** @description If `null`, no pool size is written to the project's PostgREST config and PostgREST's own default applies. The platform does not pick a value here. */
+ db_pool: number | null;
+ db_pool_acquisition_timeout: number;
+ /** @description Schemas exposed through the Data API */
+ db_schema: string;
+ max_rows: number;
+ };
+ /** @description Effective Auth config, keyed by lowercased GoTrue setting name and resolved through the `gotrue_config` view, so a setting the project has never overridden is reported at its platform default. Secrets are returned as an HMAC of their value, never in plaintext. */
+ auth: {
+ [key: string]: unknown;
+ };
+ database: {
+ /** @description The major Postgres version the database runs. `17` covers both Postgres 17 and Oriole on 17, since Oriole is a storage engine rather than a version. */
+ major_version: number;
+ network_restrictions: {
+ allowed_cidrs: {
+ address: string;
+ /** @enum {string} */
+ type: 'v4' | 'v6';
+ }[];
+ applied_at?: string;
+ /** @enum {string} */
+ entitlement: 'allowed' | 'disallowed';
+ /**
+ * @description Whether the allowlist below is applied to the project or only stored.
+ * @enum {string}
+ */
+ status: 'applied' | 'stored';
+ updated_at?: string;
+ };
+ /** @description Postgres parameter overrides. Empty when the project runs entirely on defaults. */
+ postgres_settings: {
+ /** @description Default unit: s */
+ checkpoint_timeout?: string;
+ cron_log_statement?: boolean;
+ effective_cache_size?: string;
+ hot_standby_feedback?: boolean;
+ /** @description Default unit: ms */
+ log_autovacuum_min_duration?: string;
+ log_checkpoints?: boolean;
+ log_connections?: boolean;
+ log_disconnections?: boolean;
+ log_duration?: boolean;
+ log_lock_waits?: boolean;
+ log_recovery_conflict_waits?: boolean;
+ log_replication_commands?: boolean;
+ /** @description Default unit: ms */
+ log_startup_progress_interval?: string;
+ log_temp_files?: string;
+ logical_decoding_work_mem?: string;
+ maintenance_work_mem?: string;
+ max_connections?: number;
+ max_locks_per_transaction?: number;
+ max_logical_replication_workers?: number;
+ max_parallel_maintenance_workers?: number;
+ max_parallel_workers?: number;
+ max_parallel_workers_per_gather?: number;
+ max_replication_slots?: number;
+ max_slot_wal_keep_size?: string;
+ max_standby_archive_delay?: string;
+ max_standby_streaming_delay?: string;
+ max_sync_workers_per_subscription?: number;
+ max_wal_senders?: number;
+ max_wal_size?: string;
+ max_worker_processes?: number;
+ /** @enum {string} */
+ session_replication_role?: 'local' | 'origin' | 'replica';
+ shared_buffers?: string;
+ /** @description Default unit: ms */
+ statement_timeout?: string;
+ track_activity_query_size?: string;
+ track_commit_timestamp?: boolean;
+ wal_keep_size?: string;
+ /** @description Default unit: ms */
+ wal_sender_timeout?: string;
+ work_mem?: string;
+ };
+ /** @description Whether the database rejects plaintext connections */
+ ssl_enforced: boolean;
+ };
+ pooler: {
+ /** @description Defaults to the pooler's size for the project's compute when not overridden. */
+ default_pool_size: number;
+ ignore_startup_parameters: string;
+ /** @description Defaults to the pooler's size for the project's compute when not overridden. */
+ max_client_conn: number;
+ /** @enum {string} */
+ pool_mode: 'session' | 'statement' | 'transaction';
+ query_wait_timeout: number;
+ reserve_pool_size: number;
+ server_idle_timeout: number;
+ server_lifetime: number;
+ };
+ realtime: {
+ /** @description Defaults to Realtime's pool size for the project's compute when not overridden. */
+ connection_pool: number;
+ max_bytes_per_second: number;
+ max_channels_per_client: number;
+ max_concurrent_users: number;
+ max_events_per_second: number;
+ max_joins_per_second: number;
+ max_payload_size_in_kb: number;
+ max_presence_events_per_second: number;
+ /** @description If `null`, no override is stored and Realtime applies its own default. */
+ postgres_changes_pool: number | null;
+ presence_enabled: boolean;
+ private_only: boolean;
+ suspend: boolean;
+ };
+ /** @description Read from the storage service's admin API rather than the middleware DB, so unlike the rest of this resource it reflects the tenant's live config. */
+ storage: {
+ capabilities: {
+ iceberg_catalog: boolean;
+ list_v2: boolean;
+ };
+ database_pool_mode: string;
+ features: {
+ iceberg_catalog: {
+ enabled: boolean;
+ max_catalogs: number;
+ max_namespaces: number;
+ max_tables: number;
+ };
+ image_transformation: {
+ enabled: boolean;
+ };
+ purge_cache: {
+ enabled: boolean;
+ };
+ s3_protocol: {
+ enabled: boolean;
+ };
+ vector_buckets: {
+ enabled: boolean;
+ max_buckets: number;
+ max_indexes: number;
+ };
+ };
+ /** Format: int64 */
+ file_size_limit: number;
+ migration_version: string;
+ /** @enum {string} */
+ upstream_target: 'canary' | 'main';
+ };
+ };
+ /** @description Project ref. */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_config';
+ };
+ };
+ V2ProjectCreationRateResponse: {
+ data: {
+ attributes: {
+ /**
+ * @description Active projects already in the organization. On a paid plan the first one is absorbed by the organization's compute credits, so this decides whether the amount is zero.
+ * @example 1
+ */
+ active_project_count: number;
+ /**
+ * @description Authoritative rate a new project adds to the organization. Zero is authoritative and means the project can be created without an additional charge.
+ * @example 10
+ */
+ amount: number;
+ /**
+ * @description ISO 4217 currency of the amount.
+ * @example USD
+ */
+ currency: string;
+ /**
+ * @description Organization the charge lands on.
+ * @example my-org
+ */
+ organization_slug: string;
+ /**
+ * @description Plan that decided the amount.
+ * @example pro
+ */
+ plan_id: string;
+ /**
+ * @description Interval the amount recurs at.
+ * @example monthly
+ * @enum {string}
+ */
+ recurrence: 'hourly' | 'monthly';
+ } & {
+ [key: string]: unknown;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_creation_rate';
+ };
+ };
+ V2TransferProjectBody: {
+ data: {
+ attributes: {
+ target_organization_slug: string;
+ };
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_transfer_input';
+ };
+ };
+ V2WorkerResponse: {
+ data: {
+ attributes: {
+ /** @enum {string} */
+ build_state: 'active' | 'building' | 'failed';
+ deleting?: boolean;
+ image_version?: string;
+ instances?: {
+ declared: number;
+ live: number;
+ ready: number;
+ stale: number;
+ };
+ instances_error?: string;
+ secret_generation: string;
+ spec: {
+ /** @example public */
+ exposure: string;
+ /** @example 1 */
+ instances: number;
+ /** @example node */
+ runtime?: string;
+ /** @example 2gb-1vcpu */
+ size: string;
+ };
+ state_reason?: string;
+ };
+ /**
+ * @description Worker name.
+ * @example hello-world
+ */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_worker';
+ };
+ };
+ V2WorkerUploadResponse: {
+ data: {
+ attributes: {
+ /** @description When the slot stops accepting the upload. */
+ expires_at: string;
+ /** @example PUT */
+ method: string;
+ /** @description Presigned destination for the `.tar.gz` build context. */
+ url: string;
+ };
+ /**
+ * @description Upload id to pass to the deploy endpoint as `context_upload_id`.
+ * @example cafe0000000000000000000000000000
+ */
+ id: string;
+ /**
+ * @description Resource type.
+ * @enum {string}
+ */
+ type: 'project_worker_upload';
+ };
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ 'v2-list-organization-github-connections': {
+ parameters: {
+ query?: {
+ filter?: {
+ /**
+ * @description Project ref
+ * @example abcdefghijklmnopqrst
+ */
+ project_ref?: string;
+ };
+ page?: {
+ /**
+ * @description Project ref
+ * @example abcdefghijklmnopqrst
+ */
+ after?: string;
+ /**
+ * @description Project ref
+ * @example abcdefghijklmnopqrst
+ */
+ before?: string;
+ size?: number;
+ };
+ };
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListGitHubConnectionsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-organization-members': {
+ parameters: {
+ query?: {
+ filter?: {
+ /** Format: email */
+ primary_email?: string;
+ username?: string;
+ };
+ page?: {
+ /** Format: uuid */
+ after?: string;
+ /** Format: uuid */
+ before?: string;
+ size?: number;
+ };
+ };
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListMembersResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-create-organization-invitations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2CreateInvitationsRequest'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2CreateInvitationsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description This feature requires the Enterprise organization plan. */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-delete-organization-invitations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2DeleteInvitationsRequest'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2DeleteInvitationsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description This feature requires the Enterprise organization plan. */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-assign-organization-member-role': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ user_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2AssignOrganizationMemberRoleRequest'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['OrganizationMemberRoleResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description This feature requires the Enterprise organization plan. */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to assign organization member role */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-get-project-creation-rate': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ProjectCreationRateResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Organization not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-organization-projects': {
+ parameters: {
+ query?: {
+ page?: {
+ after?: string;
+ before?: string;
+ size?: number;
+ };
+ /** @description Case-insensitive substring match on the project name. */
+ search?: string;
+ /** @description Sort order by creation time: `inserted_at` (oldest first) or `-inserted_at` (newest first). Defaults to `inserted_at`. */
+ sort?: '-inserted_at' | 'inserted_at';
+ };
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListProjectsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-organization-roles': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Organization slug */
+ slug: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListRolesResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-log-drains': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ListLogDrainsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to fetch log drains */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-create-log-drain': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['CreateLogDrainRequestOpenApi'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['LogDrainResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description This feature requires the Pro, Team, or Enterprise organization plan. */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to create a log drain */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-update-log-drain': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Log drains identifier */
+ id: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['UpdateLogDrainRequestOpenApi'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['LogDrainResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to update log drain */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-delete-log-drain': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Log drains identifier */
+ id: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to delete a log drain */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-get-branch-creation-rate': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2BranchCreationRateResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Project not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-get-project-config': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ProjectConfigResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-private-link-associations': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListPrivateLinkAssociationsResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to retrieve AWS accounts for project */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-create-private-link-association': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2CreatePrivateLinkAssociationRequest'];
+ };
+ };
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2PrivateLinkAssociationResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description This feature requires the Team, or Enterprise organization plan. */
+ 402: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to add AWS account to PrivateLink share */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-delete-private-link-association': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description AWS account ID used in PrivateLink association */
+ aws_account_id: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to remove AWS account from PrivateLink share */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-delete-private-link-association-for-database': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description AWS account ID used in PrivateLink association */
+ aws_account_id: string;
+ /** @description Identifier of the read replica this PrivateLink association targets */
+ database_identifier: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Failed to remove AWS account from PrivateLink share */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-transfer-a-project': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2TransferProjectBody'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-preview-a-project-transfer': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2TransferProjectBody'];
+ };
+ };
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2PreviewProjectTransferResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-list-all-workers': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2ListWorkersResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-get-a-worker': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2WorkerResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-delete-a-worker': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-deploy-a-worker': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ 'application/json': components['schemas']['V2DeployWorkerRequest'];
+ };
+ };
+ responses: {
+ 202: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2WorkerResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+ 'v2-create-worker-upload': {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ /** @description Project ref */
+ ref: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['V2WorkerUploadResponse'];
+ };
+ };
+ /** @description Unauthorized */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Forbidden action */
+ 403: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ /** @description Rate limit exceeded */
+ 429: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ 'application/json': components['schemas']['ErrorResponseBody'];
+ };
+ };
+ };
+ };
+}
diff --git a/packages/mcp-server-supabase/src/platform/api-platform.test.ts b/packages/mcp-server-supabase/src/platform/api-platform.test.ts
new file mode 100644
index 00000000..cad2048c
--- /dev/null
+++ b/packages/mcp-server-supabase/src/platform/api-platform.test.ts
@@ -0,0 +1,156 @@
+import type { SetupServer } from 'msw/node';
+import { afterEach, beforeEach, describe, expect, test } from 'vitest';
+
+import {
+ ACCESS_TOKEN,
+ API_URL,
+ createOrganization,
+ createProject,
+ MCP_CLIENT_NAME,
+ MCP_CLIENT_VERSION,
+ MOCK_BRANCH_CREATION_RATE,
+ MOCK_PROJECT_CREATION_RATE,
+ queuedBranchCreationRates,
+ queuedProjectCreationRates,
+ setupMockApis,
+} from '../../test/mocks.js';
+import { createSupabaseApiPlatform } from './api-platform.js';
+
+/**
+ * The creation-rate reads go through the real adapter and the real v2 paths.
+ * Everywhere else these rates are stubbed at the platform seam, so this is the
+ * only place the request paths and the `data.attributes` envelope are
+ * exercised.
+ */
+
+let mockServer: SetupServer | undefined;
+
+beforeEach(() => {
+ mockServer = setupMockApis();
+});
+
+afterEach(() => {
+ mockServer?.close();
+});
+
+async function apiPlatform() {
+ const platform = createSupabaseApiPlatform({
+ accessToken: ACCESS_TOKEN,
+ apiUrl: API_URL,
+ });
+
+ // The mock API asserts the user agent on every request, which the platform
+ // sets when the server initializes.
+ await platform.init?.({
+ clientInfo: { name: MCP_CLIENT_NAME, version: MCP_CLIENT_VERSION },
+ clientCapabilities: {},
+ });
+
+ return platform;
+}
+
+async function billableOrganization() {
+ const org = await createOrganization({
+ name: 'Paid Org',
+ plan: 'pro',
+ allowed_release_channels: ['ga'],
+ });
+ // A paid plan absorbs its first active project, so the next one is billable.
+ await createProject({
+ name: 'Existing',
+ region: 'us-east-1',
+ organization_id: org.id,
+ });
+ return org;
+}
+
+describe('project creation rate', () => {
+ test('unwraps the response envelope down to the rate itself', async () => {
+ const org = await billableOrganization();
+ const platform = await apiPlatform();
+
+ const rate = await platform.account?.getProjectCreationRate(org.id);
+
+ // Exactly the rate: the applicability context the endpoint also returns
+ // (organization slug, plan, active project count) stays out of the
+ // package, because nothing here decides pricing.
+ expect(rate).toStrictEqual(MOCK_PROJECT_CREATION_RATE);
+ });
+
+ test('returns a zero amount as the authoritative rate it is', async () => {
+ const org = await createOrganization({
+ name: 'Free Org',
+ plan: 'free',
+ allowed_release_channels: ['ga'],
+ });
+ const platform = await apiPlatform();
+
+ expect(
+ await platform.account?.getProjectCreationRate(org.id)
+ ).toStrictEqual({ ...MOCK_PROJECT_CREATION_RATE, amount: 0 });
+ });
+
+ test('carries the currency and recurrence the endpoint reports', async () => {
+ const org = await billableOrganization();
+ const platform = await apiPlatform();
+ // Nothing about the mapping is hard-coded, so a rate in another currency
+ // at another interval arrives unchanged.
+ queuedProjectCreationRates.push({
+ amount: 0.5,
+ currency: 'EUR',
+ recurrence: 'hourly',
+ });
+
+ expect(
+ await platform.account?.getProjectCreationRate(org.id)
+ ).toStrictEqual({ amount: 0.5, currency: 'EUR', recurrence: 'hourly' });
+ });
+
+ test('a UUID-shaped identifier is not an organization slug', async () => {
+ const platform = await apiPlatform();
+
+ // The v2 route resolves a slug and nothing else. A client's
+ // `organization_id` already holds a slug, because v1 keeps `id` as a
+ // deprecated alias of it, so a UUID reaching here is a caller bug and
+ // surfaces as the platform's own not-found error rather than a rate.
+ await expect(
+ platform.account?.getProjectCreationRate(
+ '3f1c9a44-6f2e-4d3b-9c8a-1e5b7d0f2a61'
+ )
+ ).rejects.toThrowError('Organization not found');
+ });
+});
+
+describe('branch creation rate', () => {
+ test('reads the rate for the parent project, hourly', async () => {
+ const org = await billableOrganization();
+ const project = await createProject({
+ name: 'Parent',
+ region: 'us-east-1',
+ organization_id: org.id,
+ });
+ const platform = await apiPlatform();
+
+ expect(
+ await platform.branching?.getBranchCreationRate(project.id)
+ ).toStrictEqual(MOCK_BRANCH_CREATION_RATE);
+ });
+
+ test('returns a zero amount as the authoritative rate it is', async () => {
+ const org = await billableOrganization();
+ const project = await createProject({
+ name: 'Parent',
+ region: 'us-east-1',
+ organization_id: org.id,
+ });
+ const platform = await apiPlatform();
+ queuedBranchCreationRates.push({
+ ...MOCK_BRANCH_CREATION_RATE,
+ amount: 0,
+ });
+
+ expect(
+ await platform.branching?.getBranchCreationRate(project.id)
+ ).toStrictEqual({ ...MOCK_BRANCH_CREATION_RATE, amount: 0 });
+ });
+});
diff --git a/packages/mcp-server-supabase/src/platform/api-platform.ts b/packages/mcp-server-supabase/src/platform/api-platform.ts
index de732c3a..761ea76c 100644
--- a/packages/mcp-server-supabase/src/platform/api-platform.ts
+++ b/packages/mcp-server-supabase/src/platform/api-platform.ts
@@ -171,6 +171,33 @@ export function createSupabaseApiPlatform(
assertSuccess(response, 'Failed to restore project');
},
+ /**
+ * @param organizationId The organization's slug. The Management API
+ * reports a slug under both names: v1 marks `OrganizationResponseV1.id`
+ * and the create-project body's `organization_id` deprecated aliases of
+ * `slug`, kept for backwards compatibility, and the v1 controller returns
+ * `id: organization.slug`. A client's `organization_id` therefore already
+ * holds a slug, which is why it interpolates straight into the v2 path.
+ * Do not "fix" this into a UUID lookup.
+ */
+ async getProjectCreationRate(organizationId: string) {
+ const response = await managementApiClient.GET(
+ '/v2/organizations/{slug}/project-creation-rate',
+ {
+ params: {
+ path: {
+ slug: organizationId,
+ },
+ },
+ }
+ );
+
+ assertSuccess(response, 'Failed to fetch the project creation rate');
+
+ const { amount, currency, recurrence } = response.data.data.attributes;
+
+ return { amount, currency, recurrence };
+ },
};
const database: DatabaseOperations = {
@@ -750,6 +777,28 @@ export function createSupabaseApiPlatform(
assertSuccess(response, 'Failed to rebase branch');
},
+ async getBranchCreationRate(projectId: string) {
+ const response = await managementApiClient.GET(
+ '/v2/projects/{ref}/branch-creation-rate',
+ {
+ params: {
+ path: {
+ ref: projectId,
+ },
+ },
+ }
+ );
+
+ assertProjectScopedSuccess(
+ response,
+ 'Failed to fetch the branch creation rate',
+ projectId
+ );
+
+ const { amount, currency, recurrence } = response.data.data.attributes;
+
+ return { amount, currency, recurrence };
+ },
};
const storage: StorageOperations = {
diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts
index b8c3aa2b..80fe36f3 100644
--- a/packages/mcp-server-supabase/src/platform/types.ts
+++ b/packages/mcp-server-supabase/src/platform/types.ts
@@ -158,6 +158,22 @@ export const generateTypescriptTypesResultSchema = z.object({
types: z.string(),
});
+/**
+ * The authoritative rate creating one resource adds to an account, as the
+ * Management API reports it.
+ *
+ * Zero is authoritative: it means the resource can be created without an
+ * additional charge, not that the rate is unknown. This package holds no
+ * fallback price of its own, so a platform that cannot report a rate blocks
+ * creation instead of guessing one.
+ */
+export const creationRateSchema = z.object({
+ amount: z.number(),
+ /** ISO 4217 currency of the amount. */
+ currency: z.string(),
+ recurrence: z.enum(['hourly', 'monthly']),
+});
+
export type Organization = z.infer;
export type Project = z.infer;
export type Branch = z.infer;
@@ -182,6 +198,7 @@ export type QueryLogsOptions = z.infer;
export type GenerateTypescriptTypesResult = z.infer<
typeof generateTypescriptTypesResultSchema
>;
+export type CreationRate = z.infer;
export type StorageConfig = z.infer;
export type StorageBucket = z.infer;
@@ -203,6 +220,11 @@ export type AccountOperations = {
createProject(options: CreateProjectOptions): Promise;
pauseProject(projectId: string): Promise;
restoreProject(projectId: string): Promise;
+ /**
+ * Authoritative rate the next project adds to this organization, read both
+ * for the initial proposal and immediately before creation.
+ */
+ getProjectCreationRate(organizationId: string): Promise;
};
export type EdgeFunctionsOperations = {
@@ -273,6 +295,11 @@ export type BranchingOperations = {
mergeBranch(branchId: string): Promise;
resetBranch(branchId: string, options: ResetBranchOptions): Promise;
rebaseBranch(branchId: string): Promise;
+ /**
+ * Authoritative rate one new branch adds to this project's organization,
+ * read both for the initial proposal and immediately before creation.
+ */
+ getBranchCreationRate(projectId: string): Promise;
};
export type SupabasePlatform = {
diff --git a/packages/mcp-server-supabase/src/policies/cost-confirmation.test.ts b/packages/mcp-server-supabase/src/policies/cost-confirmation.test.ts
new file mode 100644
index 00000000..1541100b
--- /dev/null
+++ b/packages/mcp-server-supabase/src/policies/cost-confirmation.test.ts
@@ -0,0 +1,301 @@
+import type { InputResponseView } from '@modelcontextprotocol/server';
+import { describe, expect, test, vi } from 'vitest';
+
+import type { CreationRate } from '../platform/types.js';
+import {
+ APPROVED_RATE_STALE,
+ assertRateStillApproved,
+ costConfirmationMessage,
+ createCostConfirmationPolicy,
+ creationOutcomeMessage,
+ POLICY_VERSION,
+ type CostConfirmationProposal,
+ type CostConfirmationPolicyOptions,
+} from './cost-confirmation.js';
+
+type Args = { name: string; organization_id: string };
+
+const BILLABLE: CreationRate = {
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+};
+
+const PROPOSAL: CostConfirmationProposal = {
+ action: 'create_project',
+ resourceName: 'demo',
+ account: { type: 'organization', id: 'acme' },
+ rate: BILLABLE,
+};
+
+function policyFor(rate: CreationRate) {
+ const readRate = vi.fn(async () => rate);
+ const options: CostConfirmationPolicyOptions = {
+ action: 'create_project',
+ canonicalArguments: ({ name, organization_id }) => ({
+ name,
+ organization_id,
+ }),
+ subject: ({ name, organization_id }) => ({
+ resourceName: name,
+ account: { type: 'organization', id: organization_id },
+ }),
+ readRate,
+ };
+
+ return { policy: createCostConfirmationPolicy(options), readRate };
+}
+
+/** One embedded request, so its key is whatever the policy asked under. */
+function onlyRequest(proposal: CostConfirmationProposal) {
+ const requests = createCostConfirmationPolicy({
+ action: proposal.action,
+ canonicalArguments: (args) => args,
+ subject: () => ({
+ resourceName: proposal.resourceName,
+ account: proposal.account,
+ }),
+ readRate: async () => proposal.rate,
+ }).inputRequests(proposal);
+
+ const entries = Object.entries(requests);
+ expect(entries).toHaveLength(1);
+ return entries[0]!;
+}
+
+async function answerWith(
+ proposal: CostConfirmationProposal,
+ answer: InputResponseView
+) {
+ const [key] = onlyRequest(proposal);
+ const { policy } = policyFor(proposal.rate);
+ return policy.resolve(proposal, { [key]: answer });
+}
+
+/** Narrows one embedded request down to the schema it asked with. */
+function requestedSchemaOf(request: unknown): {
+ properties: Record;
+ required?: unknown;
+} {
+ if (
+ request === null ||
+ typeof request !== 'object' ||
+ !('params' in request) ||
+ request.params === null ||
+ typeof request.params !== 'object' ||
+ !('requestedSchema' in request.params)
+ ) {
+ throw new Error('embedded request carried no requested schema');
+ }
+
+ const schema = request.params.requestedSchema;
+ if (
+ schema === null ||
+ typeof schema !== 'object' ||
+ !('properties' in schema) ||
+ schema.properties === null ||
+ typeof schema.properties !== 'object'
+ ) {
+ throw new Error('requested schema carried no properties object');
+ }
+
+ return {
+ properties: schema.properties as Record,
+ required: 'required' in schema ? schema.required : undefined,
+ };
+}
+
+describe('authoritative rates', () => {
+ test('an authoritative rate reaches the proposal with its currency and recurrence', async () => {
+ const hourly: CreationRate = {
+ amount: 0.01344,
+ currency: 'USD',
+ recurrence: 'hourly',
+ };
+ const { policy, readRate } = policyFor(hourly);
+
+ const preparation = await policy.prepare({
+ name: 'demo',
+ organization_id: 'acme',
+ });
+
+ expect(readRate).toHaveBeenCalledTimes(1);
+ expect(preparation).toStrictEqual({
+ type: 'elicit',
+ proposal: {
+ action: 'create_project',
+ resourceName: 'demo',
+ account: { type: 'organization', id: 'acme' },
+ rate: hourly,
+ },
+ });
+ });
+
+ test('a zero authoritative rate executes without asking, and still carries the ceiling', async () => {
+ const free: CreationRate = {
+ amount: 0,
+ currency: 'USD',
+ recurrence: 'monthly',
+ };
+ const { policy } = policyFor(free);
+
+ // The ceiling travels into execution even though nothing was asked, which
+ // is what keeps the check before creation meaningful on this lane.
+ expect(
+ await policy.prepare({ name: 'demo', organization_id: 'acme' })
+ ).toStrictEqual({
+ type: 'execute',
+ resolution: { maximumCreationRate: free },
+ });
+ });
+});
+
+describe('action-only consent', () => {
+ test('the confirmation asks for no properties', () => {
+ const [, request] = onlyRequest(PROPOSAL);
+
+ // A property-less schema is the whole mechanism: with no field to fill in,
+ // no response content can stand in for the caller's answer.
+ expect(request).toMatchObject({
+ method: 'elicitation/create',
+ params: {
+ mode: 'form',
+ requestedSchema: { type: 'object', properties: {} },
+ },
+ });
+
+ const schema = requestedSchemaOf(request);
+ expect(Object.keys(schema.properties)).toEqual([]);
+ expect(schema.required).toBeUndefined();
+ });
+
+ test('accept grants consent even when the response body says otherwise', async () => {
+ expect(
+ await answerWith(PROPOSAL, {
+ kind: 'elicit',
+ action: 'accept',
+ content: { confirm: false, approved: 'no' },
+ })
+ ).toStrictEqual({
+ type: 'execute',
+ resolution: { maximumCreationRate: BILLABLE },
+ });
+ });
+
+ test('decline and cancel never grant consent, and stay distinct', async () => {
+ const declined = await answerWith(PROPOSAL, {
+ kind: 'elicit',
+ action: 'decline',
+ content: { confirm: true },
+ });
+ const cancelled = await answerWith(PROPOSAL, {
+ kind: 'elicit',
+ action: 'cancel',
+ content: { confirm: true },
+ });
+
+ expect(declined.type).toBe('declined');
+ expect(cancelled.type).toBe('cancelled');
+ });
+
+ test('an unanswered confirmation asks again instead of assuming', async () => {
+ expect(await answerWith(PROPOSAL, { kind: 'missing' })).toStrictEqual({
+ type: 'reissue',
+ });
+ });
+
+ test('the policy version is 2, bound into every proposal it signs', () => {
+ const { policy } = policyFor(BILLABLE);
+
+ // Version 1 read consent out of the response body. The runtime rejects a
+ // version it does not own before interpreting any response, so this number
+ // is what keeps a v1 answer from authorizing a v2 execution and back.
+ expect(POLICY_VERSION).toBe(2);
+ expect(policy.version).toBe(POLICY_VERSION);
+ });
+});
+
+describe('draft copy', () => {
+ // Swap this test for one approved-copy contract when Design and PM sign off
+ // (root gate M1). It pins the facts, never the wording around them.
+ test('the confirmation states the facts a caller decides on', () => {
+ const message = costConfirmationMessage(PROPOSAL);
+
+ expect(message).toContain('project');
+ expect(message).toContain('"demo"');
+ expect(message).toContain('acme');
+ expect(message).toContain('10 USD');
+ expect(message).toContain('per month');
+ expect(message).toContain('recurs until');
+ expect(message).toContain('Accept');
+ expect(message).toContain('decline');
+ });
+
+ test('the projection slot stays empty until Billing approves a convention', () => {
+ // Root gate M2. A total over time needs an hours-per-month convention this
+ // package must not invent, so the message says the rate and its interval
+ // and stops there. No hours constant appears anywhere in it.
+ const hourly = costConfirmationMessage({
+ ...PROPOSAL,
+ action: 'create_branch',
+ account: { type: 'parent_project', id: 'parent-ref' },
+ rate: { amount: 0.01344, currency: 'USD', recurrence: 'hourly' },
+ });
+
+ expect(hourly).toContain('0.01344 USD per hour');
+ expect(hourly).not.toMatch(/\b(720|730|744)\b/);
+ expect(hourly).not.toMatch(/per month/);
+ });
+
+ test('a creation reports what the client said, or that nothing was asked', () => {
+ const accepted = creationOutcomeMessage('create_project', 'demo', BILLABLE);
+ const unprompted = creationOutcomeMessage('create_branch', 'develop', {
+ amount: 0,
+ currency: 'USD',
+ recurrence: 'hourly',
+ });
+
+ // Client-reported, never a claim that a person saw the prompt.
+ expect(accepted).toContain('The client reported');
+ expect(accepted).toContain('was created');
+ // Nothing was asked on a zero rate, so nothing may be reported as accepted.
+ expect(unprompted).not.toContain('client reported');
+ expect(unprompted).toContain('no confirmation was requested');
+ expect(unprompted).toContain('was created');
+ });
+});
+
+describe('approved ceiling', () => {
+ const approved: CreationRate = {
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+ };
+
+ test('an equal or lower rate proceeds', () => {
+ expect(() => assertRateStillApproved(approved, approved)).not.toThrow();
+ expect(() =>
+ assertRateStillApproved({ ...approved, amount: 4 }, approved)
+ ).not.toThrow();
+ });
+
+ test('a higher rate, a changed recurrence, or a changed currency does not', () => {
+ // A smaller number under a different interval or currency is not a lower
+ // price, so neither may spend an approval.
+ for (const stale of [
+ { ...approved, amount: 11 },
+ { ...approved, amount: 1, recurrence: 'hourly' as const },
+ { ...approved, amount: 1, currency: 'EUR' },
+ ]) {
+ expect(() => assertRateStillApproved(stale, approved)).toThrowError(
+ new RegExp(APPROVED_RATE_STALE)
+ );
+ }
+ });
+
+ test('the refusal states that nothing was created', () => {
+ expect(() =>
+ assertRateStillApproved({ ...approved, amount: 11 }, approved)
+ ).toThrowError(/nothing was created/);
+ });
+});
diff --git a/packages/mcp-server-supabase/src/policies/cost-confirmation.ts b/packages/mcp-server-supabase/src/policies/cost-confirmation.ts
new file mode 100644
index 00000000..159b9956
--- /dev/null
+++ b/packages/mcp-server-supabase/src/policies/cost-confirmation.ts
@@ -0,0 +1,492 @@
+import { inputRequired } from '@modelcontextprotocol/server';
+import type {
+ ToolPolicy,
+ ToolPolicyDecision,
+ ToolRequestContext,
+} from '@supabase/mcp-utils';
+
+import type {
+ ElicitationPolicy,
+ ElicitationPreparation,
+} from '../elicitations/policy.js';
+import type { VerifiedContinuation } from '../elicitations/state.js';
+import { recoveryResult } from '../elicitations/terminal.js';
+import type { CreationRate } from '../platform/types.js';
+import { isRateWithinApproved } from '../pricing.js';
+
+/**
+ * Stable identifier for a creation refused because the authoritative rate
+ * moved past what was approved. It travels inside the caller-facing text so
+ * one string serves both a reader and a log search.
+ */
+export const APPROVED_RATE_STALE = 'approved_rate_stale';
+
+/**
+ * Refuses a creation the approved ceiling no longer covers.
+ *
+ * Both paid tools call this immediately before their creation call, with a
+ * rate read at that moment. Nothing runs between the read and the side effect,
+ * so an approval cannot be spent at a price the caller never saw.
+ */
+export function assertRateStillApproved(
+ rate: CreationRate,
+ approved: ApprovedCreationRate
+): void {
+ if (isRateWithinApproved(rate, approved)) {
+ return;
+ }
+
+ throw new Error(
+ `The authoritative cost changed after it was approved (${APPROVED_RATE_STALE}), so nothing was created. Run the tool again to review the current cost.`
+ );
+}
+
+/**
+ * Version of the cost confirmation contract.
+ *
+ * Version 1 read consent from a Boolean field in the response body. Version 2
+ * reads it from the wire action alone, so the two cannot interpret each
+ * other's state: the runtime rejects a version it does not own before it looks
+ * at any response, which is what makes a rolling deployment safe in both
+ * directions.
+ */
+export const POLICY_VERSION = 2;
+
+/** Stable policy identity bound into continuation state. */
+export const COST_CONFIRMATION_POLICY_ID = 'supabase.cost_confirmation';
+
+/**
+ * The single embedded request key. One request means one answer, so there is
+ * never a question about which response carried the consent.
+ */
+const CONSENT_REQUEST = 'cost_confirmation';
+
+/** The rate ceiling a caller approved, and the currency and interval it holds for. */
+export type ApprovedCreationRate = CreationRate;
+
+/**
+ * All a guarded tool learns from the policy: the maximum rate it may create at.
+ *
+ * No protocol fact, client label, or response content reaches business
+ * execution through this type.
+ */
+export type CostConfirmationResolution = {
+ maximumCreationRate: ApprovedCreationRate;
+};
+
+/** What is being created, in the caller's own terms. */
+export type CostConfirmationSubject = {
+ action: 'create_project' | 'create_branch';
+ /** Name the resource will be created under. */
+ resourceName: string;
+ /** Where the charge lands. */
+ account:
+ | { type: 'organization'; id: string }
+ | { type: 'parent_project'; id: string };
+};
+
+/**
+ * The facts a caller consents to, signed into continuation state so the answer
+ * resolves against the proposal that was shown rather than a fresh one.
+ */
+export type CostConfirmationProposal = CostConfirmationSubject & {
+ rate: CreationRate;
+};
+
+/**
+ * States the authoritative rate exactly as the Management API reports it:
+ * amount, currency, and the interval it recurs at.
+ *
+ * This is a rate, not a projection over time. Extrapolating it into a total
+ * needs an hours-per-month convention that Billing has not approved, and this
+ * package must not invent one.
+ */
+export function rateStatement(rate: CreationRate): string {
+ const interval = rate.recurrence === 'hourly' ? 'per hour' : 'per month';
+ return `${rate.amount} ${rate.currency} ${interval}`;
+}
+
+/**
+ * The Billing-approved projection, which does not exist yet (root gate M2).
+ *
+ * The slot is deliberately empty rather than filled with something plausible:
+ * a projected total Billing has not approved would be this package asserting a
+ * price of its own. When the convention is approved, this function returns the
+ * approved sentence, the message picks it up with no other change, and the one
+ * owning copy test swaps with it.
+ */
+function projectionStatement(_rate: CreationRate): string | undefined {
+ return undefined;
+}
+
+/**
+ * Draft confirmation copy, pending Design and PM approval.
+ *
+ * The facts are load bearing and are pinned by one test: the action, the
+ * resource, where the charge lands, the Management API rate with its
+ * recurrence, that the charge recurs until the resource is deleted, and what
+ * accepting and declining do. The wording around them is a placeholder. It
+ * never depends on how a client labels its buttons, because a caller reading
+ * only this message still knows what accepting means.
+ */
+export function costConfirmationMessage(
+ proposal: CostConfirmationProposal
+): string {
+ const resource = proposal.action === 'create_project' ? 'project' : 'branch';
+ const where =
+ proposal.account.type === 'organization'
+ ? `in organization ${proposal.account.id}`
+ : `on project ${proposal.account.id}`;
+ const projection = projectionStatement(proposal.rate);
+
+ return [
+ `Creating the ${resource} "${proposal.resourceName}" ${where}`,
+ `adds ${rateStatement(proposal.rate)}, and that charge recurs until the ${resource} is deleted.`,
+ ...(projection === undefined ? [] : [projection]),
+ 'Accept to create it now, or decline to leave it uncreated.',
+ ].join(' ');
+}
+
+/**
+ * Draft terminal copy, pending the same approval.
+ *
+ * It reports what the client said and what the server did, and claims nothing
+ * about a person: a client can answer without ever showing a human the prompt.
+ */
+function terminalMessage(
+ proposal: CostConfirmationProposal,
+ outcome: 'declined' | 'cancelled'
+): string {
+ const resource = proposal.action === 'create_project' ? 'project' : 'branch';
+ const reported =
+ outcome === 'declined'
+ ? 'The client reported that the cost was declined'
+ : 'The client dismissed the request without answering it';
+
+ return `${reported}, so no ${resource} was created.`;
+}
+
+/**
+ * Explicit text for a creation that went ahead, stating what the client
+ * reported and what the server did.
+ *
+ * The two lanes get different sentences because only one of them asked
+ * anything: a zero authoritative rate is created without a prompt, so claiming
+ * an acceptance there would be a claim about an exchange that never happened.
+ * Neither sentence claims a person saw the prompt, because a client can answer
+ * on its own.
+ *
+ * Draft copy, pending the same Design and PM approval as the rest.
+ */
+export function creationOutcomeMessage(
+ action: CostConfirmationSubject['action'],
+ resourceName: string,
+ approved: ApprovedCreationRate
+): string {
+ const resource = action === 'create_project' ? 'project' : 'branch';
+
+ if (approved.amount === 0) {
+ return `The authoritative rate for this ${resource} is ${rateStatement(approved)}, so no confirmation was requested. The ${resource} "${resourceName}" was created.`;
+ }
+
+ return `The client reported that ${rateStatement(approved)} was accepted. The ${resource} "${resourceName}" was created.`;
+}
+
+export type CreationOutcomeText = {
+ /** Notes the ceiling one creation executed under, keyed by its own result. */
+ record(
+ result: object,
+ resolution: CostConfirmationResolution | undefined
+ ): void;
+ /** Renders that creation's text. */
+ render(result: object, resourceName: string): string;
+};
+
+/**
+ * Carries the approved ceiling from a creation to the text rendered for it.
+ *
+ * Result text is rendered by a hook that receives only the business output, so
+ * the rate that decided the wording travels here instead. The key is the
+ * output's JSON bytes rather than the result object: the server validates a
+ * round-tripped snapshot and hands the text hook its own copy, so no object
+ * identity survives from `execute` to `render`. Those bytes are an exact key,
+ * because the same server refuses to emit output its schema did not accept
+ * as-is, which makes the rendered copy byte-identical to the recorded result.
+ *
+ * Nothing is added to that object, so what reaches the wire is unchanged, and
+ * a request that is not normalized never renders text at all. A recorded
+ * ceiling is consumed once: the normalized lane is the only one that records,
+ * and it always renders.
+ */
+export function createCreationOutcomeText(
+ action: CostConfirmationSubject['action']
+): CreationOutcomeText {
+ const approved = new Map();
+
+ return {
+ record(result, resolution) {
+ if (resolution !== undefined) {
+ approved.set(JSON.stringify(result), resolution.maximumCreationRate);
+ }
+ },
+
+ render(result, resourceName) {
+ const key = JSON.stringify(result);
+ const ceiling = approved.get(key);
+ approved.delete(key);
+ // No ceiling means this creation did not come through the policy, so it
+ // keeps the single-encoded rendering every caller has always received.
+ return ceiling === undefined
+ ? JSON.stringify(result)
+ : creationOutcomeMessage(action, resourceName, ceiling);
+ },
+ };
+}
+
+export type CostConfirmationPolicyOptions = {
+ action: CostConfirmationSubject['action'];
+ /** Business arguments the approval binds to, without the legacy token. */
+ canonicalArguments(args: Args): unknown;
+ /** What the caller is being asked about. */
+ subject(args: Args): Omit;
+ /** The authoritative rate for this creation, read from the Management API. */
+ readRate(args: Args): Promise;
+};
+
+/**
+ * The Supabase cost policy: one authoritative rate, one action-only question,
+ * one approved ceiling handed to the tool.
+ *
+ * State integrity, lifetime, correlation and terminal composition belong to
+ * the runtime this policy is handed to. What lives here is the product half:
+ * which rate applies, whether anything needs asking, and what an answer means.
+ */
+export function createCostConfirmationPolicy(
+ options: CostConfirmationPolicyOptions
+): ElicitationPolicy<
+ Args,
+ CostConfirmationProposal,
+ CostConfirmationResolution
+> {
+ return {
+ id: COST_CONFIRMATION_POLICY_ID,
+ version: POLICY_VERSION,
+ canonicalArguments: options.canonicalArguments,
+
+ async prepare(
+ args
+ ): Promise<
+ ElicitationPreparation<
+ CostConfirmationProposal,
+ CostConfirmationResolution
+ >
+ > {
+ const rate = await options.readRate(args);
+
+ // Zero is an authoritative answer, not a missing one: there is nothing
+ // to consent to. The rate still travels into execution as the approved
+ // ceiling, so the check immediately before creation still catches a rate
+ // that moved.
+ if (rate.amount === 0) {
+ return { type: 'execute', resolution: { maximumCreationRate: rate } };
+ }
+
+ return {
+ type: 'elicit',
+ proposal: { action: options.action, ...options.subject(args), rate },
+ };
+ },
+
+ inputRequests(proposal) {
+ return {
+ [CONSENT_REQUEST]: inputRequired.elicit({
+ message: costConfirmationMessage(proposal),
+ // Property-less by contract: with no properties there is no field a
+ // client could fill in, so consent can only come from the action.
+ requestedSchema: { type: 'object', properties: {} },
+ }),
+ };
+ },
+
+ async resolve(proposal, inputResponses) {
+ const answer = inputResponses[CONSENT_REQUEST];
+
+ // No answer to the question that was asked. Asking again is the only
+ // safe reading: a missing response is not consent, and it is not a
+ // refusal either.
+ if (answer === undefined || answer.kind !== 'elicit') {
+ return { type: 'reissue' };
+ }
+
+ switch (answer.action) {
+ case 'accept':
+ // The wire action is the whole answer. Response content is never
+ // read, so no Boolean, string, or absent field can grant, weaken, or
+ // withdraw this consent.
+ return {
+ type: 'execute',
+ resolution: { maximumCreationRate: proposal.rate },
+ };
+ case 'decline':
+ return {
+ type: 'declined',
+ message: terminalMessage(proposal, 'declined'),
+ };
+ case 'cancel':
+ return {
+ type: 'cancelled',
+ message: terminalMessage(proposal, 'cancelled'),
+ };
+ default:
+ return { type: 'reissue' };
+ }
+ },
+ };
+}
+
+/**
+ * Guidance for a capable client that called the retired confirmation tool by
+ * name. Draft copy, pending the same approval as the rest.
+ */
+const MIGRATION_GUIDANCE =
+ 'This client confirms costs inside the tool that creates the resource. Call create_project or create_branch directly and answer the confirmation it requests; a separate confirmation ID is not needed.';
+
+/** The legacy confirmation token, split out of the business arguments. */
+export const LEGACY_CONFIRMATION_FIELD = 'confirm_cost_id';
+
+const LEGACY_TELEMETRY = {
+ policyId: COST_CONFIRMATION_POLICY_ID,
+ policyVersion: POLICY_VERSION,
+ authorityPath: 'legacy_confirmation',
+} as const;
+
+/**
+ * Chooses the authority path for one paid creation call, before the
+ * elicitation runtime is consulted.
+ *
+ * A request that cannot carry a form takes the legacy lane it has always
+ * taken: the same required token, the same schema, the same check inside the
+ * tool. Only a request that can carry a form reaches the runtime. The
+ * runtime's own refusal for an incapable client stays as a backstop for a
+ * consumer that attaches this policy where forms cannot be delivered; it is
+ * never how a legacy caller is routed.
+ */
+export function routeCostConfirmation(options: {
+ capable(ctx: ToolRequestContext): boolean;
+ confirmed: ToolPolicy;
+}): ToolPolicy {
+ const { capable, confirmed } = options;
+
+ /**
+ * Whether the runtime owns this request.
+ *
+ * A request carrying verified continuation state belongs to a flow that
+ * already started, so it stays on the confirmed lane even if this leg is no
+ * longer capable. Handing it to the legacy lane instead would demand a token
+ * the caller was never given and answer capability loss with a hash
+ * mismatch, where the runtime answers it with recovery text. State that
+ * fails integrity, actor, or method binding never reaches here at all.
+ */
+ const runtimeOwns = (ctx: ToolRequestContext) =>
+ ctx.server.mcpReq.requestState() !== undefined ||
+ capable(ctx);
+
+ return {
+ inputSchema(schema, ctx) {
+ const contextual = confirmed.inputSchema?.(schema, ctx) ?? schema;
+
+ // The legacy token is not part of the modern contract, so a capable
+ // client is never shown a field it must not use.
+ return runtimeOwns(ctx)
+ ? contextual.omit({ [LEGACY_CONFIRMATION_FIELD]: true })
+ : contextual;
+ },
+
+ outputSchema(schema, ctx) {
+ // Composed, never re-decided, and never defaulted: the runtime widens
+ // the schema for a request that can reach a terminal outcome and
+ // returns undefined for one that cannot. Passing that undefined through
+ // is what holds a legacy request on its pre-normalization bytes.
+ return confirmed.outputSchema?.(schema, ctx);
+ },
+
+ normalizeArguments(raw, ctx) {
+ const normalized = confirmed.normalizeArguments?.(raw, ctx) ?? raw;
+
+ if (
+ !runtimeOwns(ctx) ||
+ normalized === null ||
+ typeof normalized !== 'object'
+ ) {
+ return normalized;
+ }
+
+ // Dropped before canonicalization, so a token supplied anyway is not
+ // part of what an approval binds to and cannot stand in for one.
+ const { [LEGACY_CONFIRMATION_FIELD]: _ignored, ...business } =
+ normalized as Record;
+
+ return business;
+ },
+
+ async resolve(
+ args,
+ ctx
+ ): Promise> {
+ if (runtimeOwns(ctx)) {
+ return confirmed.resolve(args, ctx);
+ }
+
+ // No resolution: the tool falls back to the legacy token check it has
+ // always run, and nothing about this request reaches the runtime.
+ return {
+ type: 'execute',
+ resolution: undefined,
+ telemetry: { ...LEGACY_TELEMETRY, outcome: 'executed' },
+ };
+ },
+ };
+}
+
+/**
+ * Answers a direct call to the retired confirmation tool.
+ *
+ * The handler stays registered for every caller that still needs it. A capable
+ * client that calls it by name gets guidance instead of a confirmation ID,
+ * because a token it obtained here would be ignored by the creation call
+ * anyway.
+ */
+export function routeLegacyConfirmation(options: {
+ capable(ctx: ToolRequestContext): boolean;
+}): ToolPolicy {
+ return {
+ outputSchema(schema, ctx) {
+ // An incapable caller keeps this tool exactly as it always was, which
+ // means no structured results at all. A capable caller never sees it in
+ // discovery, and its direct call is answered with guidance rather than
+ // an execution, so normalizing that request changes nothing it can
+ // observe.
+ return options.capable(ctx) ? schema : undefined;
+ },
+
+ async resolve(_args, ctx): Promise> {
+ if (options.capable(ctx)) {
+ return {
+ type: 'result',
+ result: recoveryResult(MIGRATION_GUIDANCE),
+ telemetry: {
+ ...LEGACY_TELEMETRY,
+ outcome: 'rejected',
+ reason: 'legacy_confirmation_retired',
+ },
+ };
+ }
+
+ return {
+ type: 'execute',
+ resolution: undefined,
+ telemetry: { ...LEGACY_TELEMETRY, outcome: 'executed' },
+ };
+ },
+ };
+}
diff --git a/packages/mcp-server-supabase/src/policies/legacy-cost-bytes.test.ts b/packages/mcp-server-supabase/src/policies/legacy-cost-bytes.test.ts
new file mode 100644
index 00000000..fa91f45a
--- /dev/null
+++ b/packages/mcp-server-supabase/src/policies/legacy-cost-bytes.test.ts
@@ -0,0 +1,318 @@
+import { Client } from '@modelcontextprotocol/client';
+import {
+ createMcpServer,
+ StreamTransport,
+ type Tool,
+} from '@supabase/mcp-utils';
+import { expect, test } from 'vitest';
+
+import { createElicitationRuntime } from '../elicitations/runtime.js';
+import type {
+ AccountOperations,
+ BranchingOperations,
+ CreationRate,
+} from '../platform/types.js';
+import { getAccountTools } from '../tools/account-tools.js';
+import { getBranchingTools } from '../tools/branching-tools.js';
+import {
+ createCostConfirmationPolicy,
+ routeCostConfirmation,
+ routeLegacyConfirmation,
+} from './cost-confirmation.js';
+
+/**
+ * A legacy caller must keep the bytes it has always received, whether or not
+ * this connection also serves form elicitation to modern capable clients.
+ *
+ * The expected values below were measured, not written by hand: they are the
+ * output of this exact fixture's platform objects against base main
+ * (302d2ad7870352444ca0d71711622ab38a66e4ff), whose paid tools carry no
+ * policy. Every field is fixed, so the strings are stable across runs.
+ */
+const BASE_CREATE_PROJECT_ENTRY =
+ '{"name":"create_project","description":"Creates a new Supabase project. Always ask the user which organization to create the project in. The project can take a few minutes to initialize - use `get_project` to check the status.","inputSchema":{"type":"object","properties":{"name":{"description":"The name of the project","type":"string"},"region":{"description":"The region to create the project in.","type":"string","enum":["us-west-1","us-east-1","us-east-2","ca-central-1","eu-west-1","eu-west-2","eu-west-3","eu-central-1","eu-central-2","eu-north-1","ap-south-1","ap-southeast-1","ap-northeast-1","ap-northeast-2","ap-southeast-2","sa-east-1"]},"organization_id":{"type":"string"},"confirm_cost_id":{"description":"The cost confirmation ID. Call `confirm_cost` first.","type":"string"}},"required":["name","region","organization_id","confirm_cost_id"],"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false},"annotations":{"title":"Create project","readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}}';
+
+const BASE_CREATE_BRANCH_ENTRY =
+ '{"name":"create_branch","description":"Creates a development branch on a Supabase project. This will apply all migrations from the main project to a fresh branch database. Note that production data will not carry over. The branch will get its own project_id via the resulting project_ref. Use this ID to execute queries and migrations on the branch.","inputSchema":{"type":"object","properties":{"project_id":{"type":"string"},"name":{"description":"Name of the branch to create","default":"develop","type":"string"},"confirm_cost_id":{"description":"The cost confirmation ID. Call `confirm_cost` first.","type":"string"}},"required":["project_id","name","confirm_cost_id"],"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false},"annotations":{"title":"Create branch","readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false}}';
+
+const BASE_CONFIRM_COST_ENTRY =
+ '{"name":"confirm_cost","description":"Ask the user to confirm their understanding of the cost of creating a new project or branch. Call `get_cost` first. Returns a unique ID for this confirmation which should be passed to `create_project` or `create_branch`.","inputSchema":{"type":"object","properties":{"type":{"type":"string","enum":["project","branch"]},"recurrence":{"type":"string","enum":["hourly","monthly"]},"amount":{"type":"number"}},"required":["type","recurrence","amount"],"$schema":"http://json-schema.org/draft-07/schema#","additionalProperties":false},"annotations":{"title":"Confirm cost understanding","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}}';
+
+const BASE_CONFIRM_COST_PROJECT_RESULT =
+ '{"content":[{"type":"text","text":"{\\"confirmation_id\\":\\"BGoZHqqJd2JYMt+cWSDFH7qDeNkZZAwbTytJrHy7r+E=\\"}"}]}';
+
+const BASE_CREATE_PROJECT_RESULT =
+ '{"content":[{"type":"text","text":"{\\"id\\":\\"fixed-project-ref\\",\\"ref\\":\\"fixed-project-ref\\",\\"organization_id\\":\\"fixed-org\\",\\"organization_slug\\":\\"fixed-org\\",\\"name\\":\\"Fixture Project\\",\\"status\\":\\"UNKNOWN\\",\\"created_at\\":\\"2026-01-01T00:00:00.000Z\\",\\"region\\":\\"us-east-1\\"}"}]}';
+
+const BASE_CREATE_BRANCH_RESULT =
+ '{"content":[{"type":"text","text":"{\\"id\\":\\"fixed-branch\\",\\"name\\":\\"develop\\",\\"project_ref\\":\\"fixed-branch-ref\\",\\"parent_project_ref\\":\\"fixed-project-ref\\",\\"is_default\\":false,\\"persistent\\":false,\\"status\\":\\"CREATING_PROJECT\\",\\"created_at\\":\\"2026-01-01T00:00:00.000Z\\",\\"updated_at\\":\\"2026-01-01T00:00:00.000Z\\"}"}]}';
+
+const PROJECT_RATE: CreationRate = {
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+};
+const BRANCH_RATE: CreationRate = {
+ amount: 0.01344,
+ currency: 'USD',
+ recurrence: 'hourly',
+};
+
+const FIXED_PROJECT = {
+ id: 'fixed-project-ref',
+ ref: 'fixed-project-ref',
+ organization_id: 'fixed-org',
+ organization_slug: 'fixed-org',
+ name: 'Fixture Project',
+ status: 'UNKNOWN',
+ created_at: '2026-01-01T00:00:00.000Z',
+ region: 'us-east-1',
+};
+
+const FIXED_BRANCH = {
+ id: 'fixed-branch',
+ name: 'develop',
+ project_ref: 'fixed-branch-ref',
+ parent_project_ref: 'fixed-project-ref',
+ is_default: false,
+ persistent: false,
+ status: 'CREATING_PROJECT' as const,
+ created_at: '2026-01-01T00:00:00.000Z',
+ updated_at: '2026-01-01T00:00:00.000Z',
+};
+
+const account: AccountOperations = {
+ async listOrganizations() {
+ return [{ id: 'fixed-org', slug: 'fixed-org', name: 'Fixture Org' }];
+ },
+ async getOrganization() {
+ return {
+ id: 'fixed-org',
+ name: 'Fixture Org',
+ plan: 'pro',
+ allowed_release_channels: ['ga'],
+ opt_in_tags: [],
+ };
+ },
+ async listProjects() {
+ return [FIXED_PROJECT];
+ },
+ async getProject() {
+ return FIXED_PROJECT;
+ },
+ async createProject() {
+ return FIXED_PROJECT;
+ },
+ async pauseProject() {},
+ async restoreProject() {},
+ async getProjectCreationRate() {
+ return PROJECT_RATE;
+ },
+};
+
+const branching: BranchingOperations = {
+ async listBranches() {
+ return [];
+ },
+ async createBranch() {
+ return FIXED_BRANCH;
+ },
+ async deleteBranch() {},
+ async mergeBranch() {},
+ async resetBranch() {},
+ async rebaseBranch() {},
+ async getBranchCreationRate() {
+ return BRANCH_RATE;
+ },
+};
+
+type CreateProjectArgs = {
+ name: string;
+ region: string;
+ organization_id: string;
+};
+
+type CreateBranchArgs = { project_id: string; name: string };
+
+/**
+ * Serves the real paid tools with the cost policy attached, exactly as a
+ * connection that also serves modern capable clients would.
+ */
+async function setupLegacyClient() {
+ const runtime = createElicitationRuntime({
+ actorId: 'actor-1',
+ stateKey: 'legacy-bytes-continuation-key-long-enough',
+ formDeliveryAvailable: true,
+ });
+ const capable = (ctx: Parameters[0]) =>
+ runtime.availability(ctx).formElicitation;
+
+ const accountTools = getAccountTools({ account });
+ const branchingTools = getBranchingTools({ branching });
+
+ const tools: Record> = {
+ ...accountTools,
+ ...branchingTools,
+ confirm_cost: {
+ ...accountTools.confirm_cost,
+ policy: routeLegacyConfirmation({ capable }),
+ },
+ create_project: {
+ ...accountTools.create_project,
+ policy: routeCostConfirmation({
+ capable,
+ confirmed: runtime.policy(
+ 'create_project',
+ createCostConfirmationPolicy({
+ action: 'create_project',
+ canonicalArguments: ({ name, region, organization_id }) => ({
+ name,
+ region,
+ organization_id,
+ }),
+ subject: ({ name, organization_id }) => ({
+ resourceName: name,
+ account: { type: 'organization', id: organization_id },
+ }),
+ readRate: () => account.getProjectCreationRate('fixed-org'),
+ })
+ ),
+ }),
+ },
+ create_branch: {
+ ...branchingTools.create_branch,
+ policy: routeCostConfirmation({
+ capable,
+ confirmed: runtime.policy(
+ 'create_branch',
+ createCostConfirmationPolicy({
+ action: 'create_branch',
+ canonicalArguments: ({ project_id, name }) => ({
+ project_id,
+ name,
+ }),
+ subject: ({ project_id, name }) => ({
+ resourceName: name,
+ account: { type: 'parent_project', id: project_id },
+ }),
+ readRate: ({ project_id }) =>
+ branching.getBranchCreationRate(project_id),
+ })
+ ),
+ }),
+ },
+ };
+
+ const clientTransport = new StreamTransport();
+ const serverTransport = new StreamTransport();
+ clientTransport.readable.pipeTo(serverTransport.writable);
+ serverTransport.readable.pipeTo(clientTransport.writable);
+
+ const server = createMcpServer({
+ name: 'supabase',
+ version: '0.0.0',
+ requestState: runtime.requestState,
+ tools,
+ });
+ const client = new Client(
+ { name: 'test-client', version: '1.0.0' },
+ { capabilities: {} }
+ );
+
+ await server.connect(serverTransport);
+ await client.connect(clientTransport);
+ return client;
+}
+
+async function entryOf(client: Client, name: string) {
+ const { tools } = await client.listTools();
+ return tools.find((entry) => entry.name === name);
+}
+
+/** Reads a confirmation id out of a legacy `confirm_cost` result. */
+function confirmationIdOf(result: { content?: unknown }): string {
+ const content = result.content;
+ if (!Array.isArray(content)) {
+ throw new Error('tool result carried no content');
+ }
+ const [entry] = content;
+ if (
+ entry === undefined ||
+ typeof entry !== 'object' ||
+ !('text' in entry) ||
+ typeof entry.text !== 'string'
+ ) {
+ throw new Error('tool result content is not text');
+ }
+ const parsed: unknown = JSON.parse(entry.text);
+ if (
+ parsed === null ||
+ typeof parsed !== 'object' ||
+ !('confirmation_id' in parsed) ||
+ typeof parsed.confirmation_id !== 'string'
+ ) {
+ throw new Error('tool result carried no confirmation id');
+ }
+ return parsed.confirmation_id;
+}
+
+test('a legacy caller keeps base discovery bytes for the paid tools', async () => {
+ const client = await setupLegacyClient();
+
+ expect(JSON.stringify(await entryOf(client, 'create_project'))).toBe(
+ BASE_CREATE_PROJECT_ENTRY
+ );
+ expect(JSON.stringify(await entryOf(client, 'create_branch'))).toBe(
+ BASE_CREATE_BRANCH_ENTRY
+ );
+ expect(JSON.stringify(await entryOf(client, 'confirm_cost'))).toBe(
+ BASE_CONFIRM_COST_ENTRY
+ );
+});
+
+test('a legacy caller keeps base call bytes through the confirmation pair', async () => {
+ const client = await setupLegacyClient();
+
+ const projectConfirmation = await client.callTool({
+ name: 'confirm_cost',
+ arguments: { type: 'project', recurrence: 'monthly', amount: 10 },
+ });
+ expect(JSON.stringify(projectConfirmation)).toBe(
+ BASE_CONFIRM_COST_PROJECT_RESULT
+ );
+
+ const confirmationId = confirmationIdOf(projectConfirmation);
+
+ expect(
+ JSON.stringify(
+ await client.callTool({
+ name: 'create_project',
+ arguments: {
+ name: 'Fixture Project',
+ region: 'us-east-1',
+ organization_id: 'fixed-org',
+ confirm_cost_id: confirmationId,
+ },
+ })
+ )
+ ).toBe(BASE_CREATE_PROJECT_RESULT);
+
+ const branchConfirmation = await client.callTool({
+ name: 'confirm_cost',
+ arguments: { type: 'branch', recurrence: 'hourly', amount: 0.01344 },
+ });
+ const branchConfirmationId = confirmationIdOf(branchConfirmation);
+
+ expect(
+ JSON.stringify(
+ await client.callTool({
+ name: 'create_branch',
+ arguments: {
+ project_id: 'fixed-project-ref',
+ name: 'develop',
+ confirm_cost_id: branchConfirmationId,
+ },
+ })
+ )
+ ).toBe(BASE_CREATE_BRANCH_RESULT);
+});
diff --git a/packages/mcp-server-supabase/src/pricing.ts b/packages/mcp-server-supabase/src/pricing.ts
index 960bbae3..5c44c224 100644
--- a/packages/mcp-server-supabase/src/pricing.ts
+++ b/packages/mcp-server-supabase/src/pricing.ts
@@ -1,53 +1,60 @@
-import type { AccountOperations } from './platform/types.js';
+import type { CreationRate } from './platform/types.js';
-export const PROJECT_COST_MONTHLY = 10;
-export const BRANCH_COST_HOURLY = 0.01344;
-
-export type ProjectCost = {
- type: 'project';
- recurrence: 'monthly';
- amount: number;
-};
-
-export type BranchCost = {
- type: 'branch';
- recurrence: 'hourly';
+/**
+ * The cost shape the legacy `get_cost` and `confirm_cost` pair speaks.
+ *
+ * It carries no currency, because the hash a legacy confirmation is identified
+ * by is computed over exactly these fields. Adding one would invalidate every
+ * confirmation a legacy client is holding.
+ */
+export type Cost = {
+ type: 'project' | 'branch';
+ recurrence: 'hourly' | 'monthly';
amount: number;
};
-export type Cost = ProjectCost | BranchCost;
-
/**
- * Gets the cost of the next project in an organization.
+ * Presents an authoritative rate as the legacy cost shape.
+ *
+ * This is the whole adapter: the rate itself comes from the Management API, so
+ * this package holds no price of its own to fall back to.
*/
-export async function getNextProjectCost(
- account: AccountOperations,
- orgId: string
-): Promise {
- const org = await account.getOrganization(orgId);
- const projects = await account.listProjects();
-
- const activeProjects = projects.filter(
- (project) =>
- project.organization_id === orgId &&
- !['INACTIVE', 'GOING_DOWN', 'REMOVED'].includes(project.status)
- );
-
- let amount = 0;
-
- if (org.plan !== 'free') {
- // If the organization is on a paid plan, the first project is included
- if (activeProjects.length > 0) {
- amount = PROJECT_COST_MONTHLY;
- }
- }
+export function toCost(type: Cost['type'], rate: CreationRate): Cost {
+ return { type, recurrence: rate.recurrence, amount: rate.amount };
+}
- return { type: 'project', recurrence: 'monthly', amount };
+/**
+ * The hourly branch rate the legacy confirmation pair agrees on.
+ *
+ * The authoritative branch rate is scoped to the parent project, and legacy
+ * `get_cost` has no project reference to read it with: its only argument is an
+ * organization. Both halves of the legacy pair therefore keep quoting this
+ * value, which is the same rate Billing's catalog reports today, so a legacy
+ * confirmation still matches the cost the legacy creation path recomputes.
+ *
+ * It is not a fallback: no authoritative read ever resolves to it. The v2
+ * confirmation path reads the Management API for the parent project and never
+ * calls this.
+ */
+export function legacyBranchCost(): Cost {
+ return { type: 'branch', recurrence: 'hourly', amount: 0.01344 };
}
/**
- * Gets the cost for a database branch.
+ * Whether a freshly read rate is still covered by the maximum a caller
+ * approved.
+ *
+ * An equal or lower amount proceeds; a higher amount does not. Recurrence and
+ * currency must be unchanged, because a lower number under a different
+ * interval or currency is not a lower price.
*/
-export function getBranchCost(): Cost {
- return { type: 'branch', recurrence: 'hourly', amount: BRANCH_COST_HOURLY };
+export function isRateWithinApproved(
+ rate: CreationRate,
+ approved: CreationRate
+): boolean {
+ return (
+ rate.currency === approved.currency &&
+ rate.recurrence === approved.recurrence &&
+ rate.amount <= approved.amount
+ );
}
diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts
index b411e3f0..908e7494 100644
--- a/packages/mcp-server-supabase/src/server.test.ts
+++ b/packages/mcp-server-supabase/src/server.test.ts
@@ -17,12 +17,13 @@ import {
createProject,
MCP_CLIENT_NAME,
MCP_CLIENT_VERSION,
+ MOCK_BRANCH_CREATION_RATE,
+ MOCK_PROJECT_CREATION_RATE,
mockContentApiSchemaLoadCount,
setupMockApis,
} from '../test/mocks.js';
import { createSupabaseApiPlatform } from './platform/api-platform.js';
import type { SupabasePlatform } from './platform/types.js';
-import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js';
import { createSupabaseMcpServer, instructions } from './server.js';
import {
createToolSchemas,
@@ -247,7 +248,7 @@ describe('tools', () => {
expect(result).toEqual({
type: 'project',
- amount: PROJECT_COST_MONTHLY,
+ amount: MOCK_PROJECT_CREATION_RATE.amount,
recurrence: 'monthly',
});
});
@@ -302,7 +303,7 @@ describe('tools', () => {
expect(result).toEqual({
type: 'branch',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
recurrence: 'hourly',
});
});
@@ -3078,7 +3079,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3134,7 +3135,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3206,7 +3207,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3361,7 +3362,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3466,7 +3467,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3580,7 +3581,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
@@ -3682,7 +3683,7 @@ describe('tools', () => {
arguments: {
type: 'branch',
recurrence: 'hourly',
- amount: BRANCH_COST_HOURLY,
+ amount: MOCK_BRANCH_CREATION_RATE.amount,
},
});
diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts
index 4a891659..61afe497 100644
--- a/packages/mcp-server-supabase/src/server.ts
+++ b/packages/mcp-server-supabase/src/server.ts
@@ -1,9 +1,13 @@
+import type { CallToolResult } from '@modelcontextprotocol/server';
import {
createMcpServer,
+ type McpServerOptions,
type Tool,
type ToolCallCallback,
+ type ToolRequestContext,
} from '@supabase/mcp-utils';
import packageJson from '../package.json' with { type: 'json' };
+import { createElicitationRuntime } from './elicitations/runtime.js';
import { createContentApiClient } from './content-api/index.js';
import type { SupabasePlatform } from './platform/types.js';
import { getAccountTools } from './tools/account-tools.js';
@@ -15,9 +19,59 @@ import { getDocsTools } from './tools/docs-tools.js';
import { getEdgeFunctionTools } from './tools/edge-function-tools.js';
import { getStorageTools } from './tools/storage-tools.js';
import { writeToolSet } from './tools/tool-schemas.js';
-import type { FeatureGroup } from './types.js';
+import { PLATFORM_INDEPENDENT_FEATURES, type FeatureGroup } from './types.js';
import { parseFeatureGroups } from './util.js';
+/**
+ * The dependencies a hosted deployment injects to serve cost confirmation.
+ *
+ * This is the whole supported surface, and it is deliberately smaller than
+ * what the private runtime accepts: continuation lifetime is capped by
+ * contract rather than configured, and the runtime's clock and correlation-id
+ * seams stay internal, because a caller that replaced them would break expiry
+ * classification and interaction correlation respectively.
+ */
+export type SupabaseElicitationOptions = {
+ /**
+ * Operator secret used to sign continuation state, at least 32 bytes. It
+ * never reaches a client: state is signed and readable, not encrypted.
+ */
+ stateKey: string | Uint8Array;
+
+ /** Authenticated approver every approval on this connection binds to. */
+ actorId: string;
+
+ /**
+ * Whether the serving path in front of this server can deliver a form. A
+ * path that cannot deliver one at all, such as deprecated stdio or classic
+ * hosted, is better served by omitting these options entirely.
+ */
+ formDeliveryAvailable?: boolean;
+
+ /**
+ * Connection-level form elicitation opt-out.
+ *
+ * For a modern connection whose caller opted out, pass these options with
+ * `optOut: true` rather than omitting them. Either way the tools stay on the
+ * legacy `confirm_cost` contract, but passing them keeps continuation state
+ * verified on this connection, so a flow that started before the opt-out
+ * still gets an actionable answer, and keeps an operator opt-out
+ * distinguishable from a client that never declared form support. Read the
+ * hosted URL-only opt-out on the initial leg and pass the result here.
+ */
+ optOut?: boolean;
+
+ /**
+ * Kill switch consulted immediately before protected execution. Returning a
+ * result blocks this attempt without invalidating signed state, so the same
+ * confirmation is redeemable once the gate reopens. Tools without a cost
+ * policy never reach it.
+ */
+ gate?: (
+ ctx: ToolRequestContext
+ ) => (CallToolResult & { isError: true }) | null;
+};
+
const { version } = packageJson;
export type SupabaseMcpServerOptions = {
@@ -50,6 +104,26 @@ export type SupabaseMcpServerOptions = {
*/
features?: string[];
+ /**
+ * Enables cost confirmation through form elicitation on this connection.
+ *
+ * Omit it for a serving path that cannot deliver a form at all, such as
+ * deprecated stdio or classic hosted. Every tool then keeps the surface it
+ * has today, policy-free and byte for byte. A read-only server keeps that
+ * surface too, because it creates nothing to confirm.
+ *
+ * A connection whose caller opted out passes these options with
+ * `optOut: true` rather than omitting them; see
+ * {@link SupabaseElicitationOptions.optOut} for why.
+ */
+ elicitation?: SupabaseElicitationOptions;
+
+ /**
+ * Callback for each pre-execution policy decision, carrying the allowlisted
+ * telemetry fields only.
+ */
+ onToolPolicyCall?: McpServerOptions['onToolPolicyCall'];
+
/**
* Callback for after a supabase tool is called.
*/
@@ -66,8 +140,6 @@ const DEFAULT_FEATURES: FeatureGroup[] = [
'branching',
];
-export const PLATFORM_INDEPENDENT_FEATURES: FeatureGroup[] = ['docs'];
-
export const instructions = `
Here are guidelines for using Supabase tools effectively:
@@ -96,8 +168,26 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
features,
contentApiUrl = 'https://supabase.com/docs/api/graphql',
onToolCall,
+ onToolPolicyCall,
+ elicitation: elicitationOptions,
} = options;
+ // One runtime per connection, and only when the consumer says this
+ // connection serves form elicitation. Everything downstream keys off its
+ // presence, so a consumer that injects nothing gets a policy-free server.
+ const elicitation =
+ elicitationOptions === undefined
+ ? undefined
+ : // Mapped field by field, never spread: a knob the private runtime
+ // grows stays internal until this package decides to support it.
+ createElicitationRuntime({
+ actorId: elicitationOptions.actorId,
+ stateKey: elicitationOptions.stateKey,
+ formDeliveryAvailable: elicitationOptions.formDeliveryAvailable,
+ optOut: elicitationOptions.optOut,
+ gate: elicitationOptions.gate,
+ });
+
const contentApiClientPromise = createContentApiClient(contentApiUrl, {
'User-Agent': `supabase-mcp/${version}`,
});
@@ -133,6 +223,8 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
),
]);
},
+ requestState: elicitation?.requestState,
+ onToolPolicyCall,
onToolCall,
tools: async () => {
const contentApiClient = await contentApiClientPromise;
@@ -153,7 +245,10 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
}
if (!projectId && account && enabledFeatures.has('account')) {
- Object.assign(tools, getAccountTools({ account, readOnly }));
+ Object.assign(
+ tools,
+ getAccountTools({ account, readOnly, elicitation })
+ );
}
if (database && enabledFeatures.has('database')) {
@@ -185,7 +280,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) {
if (branching && enabledFeatures.has('branching')) {
Object.assign(
tools,
- getBranchingTools({ branching, projectId, readOnly })
+ getBranchingTools({ branching, projectId, readOnly, elicitation })
);
}
diff --git a/packages/mcp-server-supabase/src/tools/account-tools.ts b/packages/mcp-server-supabase/src/tools/account-tools.ts
index e35b8ceb..5cf9b81c 100644
--- a/packages/mcp-server-supabase/src/tools/account-tools.ts
+++ b/packages/mcp-server-supabase/src/tools/account-tools.ts
@@ -1,15 +1,30 @@
-import { tool } from '@supabase/mcp-utils';
+import { tool, type ToolRequestContext } from '@supabase/mcp-utils';
import { z } from 'zod/v4';
import type { ToolDefs } from './util.js';
+import type { ElicitationRuntime } from '../elicitations/runtime.js';
import type { AccountOperations } from '../platform/types.js';
import { organizationSchema, projectSchema } from '../platform/types.js';
-import { getBranchCost, getNextProjectCost } from '../pricing.js';
+import {
+ assertRateStillApproved,
+ createCostConfirmationPolicy,
+ createCreationOutcomeText,
+ routeCostConfirmation,
+ routeLegacyConfirmation,
+ type CostConfirmationResolution,
+} from '../policies/cost-confirmation.js';
+import { legacyBranchCost, toCost } from '../pricing.js';
import { AWS_REGION_CODES } from '../regions.js';
import { hashObject } from '../util.js';
type AccountToolsOptions = {
account: AccountOperations;
readOnly?: boolean;
+ /**
+ * Present only when this connection serves form elicitation. Its absence
+ * leaves every paid tool policy-free, which is what keeps a consumer that
+ * never injects it on the exact surface it has today.
+ */
+ elicitation?: ElicitationRuntime;
};
const listOrganizationsInputSchema = z.object({});
@@ -215,7 +230,44 @@ export const accountToolDefs = {
},
} as const satisfies ToolDefs;
-export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
+export function getAccountTools({
+ account,
+ readOnly,
+ elicitation,
+}: AccountToolsOptions) {
+ // A read-only server creates nothing, so there is nothing to confirm. The
+ // paid tools stay policy-free and answer a direct call with the read-only
+ // error they always have, instead of reading a rate and prompting first.
+ const costConfirmation = readOnly === true ? undefined : elicitation;
+ const capable = (ctx: ToolRequestContext) =>
+ costConfirmation?.availability(ctx).formElicitation === true;
+ const projectOutcome = createCreationOutcomeText('create_project');
+
+ const createProjectPolicy =
+ costConfirmation &&
+ routeCostConfirmation({
+ capable,
+ confirmed: costConfirmation.policy(
+ 'create_project',
+ createCostConfirmationPolicy>({
+ action: 'create_project',
+ // The legacy token is deliberately absent: an approval binds to the
+ // project that was proposed, never to a token from the other lane.
+ canonicalArguments: ({ name, region, organization_id }) => ({
+ name,
+ region,
+ organization_id,
+ }),
+ subject: ({ name, organization_id }) => ({
+ resourceName: name,
+ account: { type: 'organization', id: organization_id },
+ }),
+ readRate: ({ organization_id }) =>
+ account.getProjectCreationRate(organization_id),
+ })
+ ),
+ });
+
return {
list_organizations: tool({
...accountToolDefs.list_organizations,
@@ -246,9 +298,12 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
execute: async ({ type, organization_id }) => {
switch (type) {
case 'project':
- return await getNextProjectCost(account, organization_id);
+ return toCost(
+ 'project',
+ await account.getProjectCreationRate(organization_id)
+ );
case 'branch':
- return getBranchCost();
+ return legacyBranchCost();
default:
throw new Error(`Unknown cost type: ${type}`);
}
@@ -256,30 +311,59 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) {
}),
confirm_cost: tool({
...accountToolDefs.confirm_cost,
+ // Hidden from a capable client's tool list, and still registered: a
+ // client that calls it by name is answered either way.
+ hidden: (ctx) => capable(ctx),
+ policy: costConfirmation && routeLegacyConfirmation({ capable }),
execute: async (cost) => {
return { confirmation_id: await hashObject(cost) };
},
}),
- create_project: tool({
+ create_project: tool<
+ typeof createProjectInputSchema,
+ typeof createProjectOutputSchema,
+ CostConfirmationResolution | undefined
+ >({
...accountToolDefs.create_project,
- execute: async ({ name, region, organization_id, confirm_cost_id }) => {
+ policy: createProjectPolicy,
+ // Rendered only for a request the policy normalized. A legacy request
+ // skips this hook entirely and keeps its single-encoded text.
+ formatResult: (project) => projectOutcome.render(project, project.name),
+ execute: async (
+ { name, region, organization_id, confirm_cost_id },
+ resolution
+ ) => {
if (readOnly) {
throw new Error('Cannot create a project in read-only mode.');
}
- const cost = await getNextProjectCost(account, organization_id);
- const costHash = await hashObject(cost);
- if (costHash !== confirm_cost_id) {
- throw new Error(
- 'Cost confirmation ID does not match the expected cost of creating a project.'
+ if (resolution === undefined) {
+ const cost = toCost(
+ 'project',
+ await account.getProjectCreationRate(organization_id)
+ );
+ const costHash = await hashObject(cost);
+ if (costHash !== confirm_cost_id) {
+ throw new Error(
+ 'Cost confirmation ID does not match the expected cost of creating a project.'
+ );
+ }
+ } else {
+ assertRateStillApproved(
+ await account.getProjectCreationRate(organization_id),
+ resolution.maximumCreationRate
);
}
- return await account.createProject({
+ const project = await account.createProject({
name,
region,
organization_id,
});
+
+ projectOutcome.record(project, resolution);
+
+ return project;
},
}),
pause_project: tool({
diff --git a/packages/mcp-server-supabase/src/tools/branching-tools.ts b/packages/mcp-server-supabase/src/tools/branching-tools.ts
index bbad5976..2fc28a89 100644
--- a/packages/mcp-server-supabase/src/tools/branching-tools.ts
+++ b/packages/mcp-server-supabase/src/tools/branching-tools.ts
@@ -1,8 +1,16 @@
-import { tool } from '@supabase/mcp-utils';
+import { tool, type ToolRequestContext } from '@supabase/mcp-utils';
import { z } from 'zod/v4';
+import type { ElicitationRuntime } from '../elicitations/runtime.js';
import type { BranchingOperations } from '../platform/types.js';
import { branchSchema } from '../platform/types.js';
-import { getBranchCost } from '../pricing.js';
+import {
+ assertRateStillApproved,
+ createCostConfirmationPolicy,
+ createCreationOutcomeText,
+ routeCostConfirmation,
+ type CostConfirmationResolution,
+} from '../policies/cost-confirmation.js';
+import { legacyBranchCost } from '../pricing.js';
import { hashObject } from '../util.js';
import { injectableTool, type ToolDefs } from './util.js';
@@ -10,6 +18,8 @@ type BranchingToolsOptions = {
branching: BranchingOperations;
projectId?: string;
readOnly?: boolean;
+ /** Present only when this connection serves form elicitation. */
+ elicitation?: ElicitationRuntime;
};
const createBranchInputSchema = z.object({
@@ -155,26 +165,74 @@ export function getBranchingTools({
branching,
projectId,
readOnly,
+ elicitation,
}: BranchingToolsOptions) {
const project_id = projectId;
+ // A read-only server creates nothing, so there is nothing to confirm.
+ const costConfirmation = readOnly === true ? undefined : elicitation;
+ const capable = (ctx: ToolRequestContext) =>
+ costConfirmation?.availability(ctx).formElicitation === true;
+ const branchOutcome = createCreationOutcomeText('create_branch');
+
+ const createBranchPolicy =
+ costConfirmation &&
+ routeCostConfirmation({
+ capable,
+ confirmed: costConfirmation.policy(
+ 'create_branch',
+ createCostConfirmationPolicy>({
+ action: 'create_branch',
+ canonicalArguments: ({ project_id, name }) => ({ project_id, name }),
+ subject: ({ project_id, name }) => ({
+ resourceName: name,
+ account: { type: 'parent_project', id: project_id },
+ }),
+ readRate: ({ project_id }) =>
+ branching.getBranchCreationRate(project_id),
+ })
+ ),
+ });
return {
- create_branch: injectableTool({
+ create_branch: injectableTool<
+ typeof createBranchInputSchema,
+ typeof createBranchOutputSchema,
+ { project_id: string | undefined },
+ CostConfirmationResolution | undefined
+ >({
...branchingToolDefs.create_branch,
+ policy: createBranchPolicy,
+ // Rendered only for a request the policy normalized. A legacy request
+ // skips this hook entirely and keeps its single-encoded text.
+ formatResult: (branch) => branchOutcome.render(branch, branch.name),
inject: { project_id },
- execute: async ({ project_id, name, confirm_cost_id }) => {
+ execute: async ({ project_id, name, confirm_cost_id }, resolution) => {
if (readOnly) {
throw new Error('Cannot create a branch in read-only mode.');
}
- const cost = getBranchCost();
- const costHash = await hashObject(cost);
- if (costHash !== confirm_cost_id) {
- throw new Error(
- 'Cost confirmation ID does not match the expected cost of creating a branch.'
+ if (resolution === undefined) {
+ const cost = legacyBranchCost();
+ const costHash = await hashObject(cost);
+ if (costHash !== confirm_cost_id) {
+ throw new Error(
+ 'Cost confirmation ID does not match the expected cost of creating a branch.'
+ );
+ }
+ } else {
+ // Read here and nowhere earlier: nothing runs between this rate and
+ // the creation call below.
+ assertRateStillApproved(
+ await branching.getBranchCreationRate(project_id),
+ resolution.maximumCreationRate
);
}
- return await branching.createBranch(project_id, { name });
+
+ const branch = await branching.createBranch(project_id, { name });
+
+ branchOutcome.record(branch, resolution);
+
+ return branch;
},
}),
list_branches: injectableTool({
diff --git a/packages/mcp-server-supabase/src/tools/util.ts b/packages/mcp-server-supabase/src/tools/util.ts
index 14cd7807..7a0c81b7 100644
--- a/packages/mcp-server-supabase/src/tools/util.ts
+++ b/packages/mcp-server-supabase/src/tools/util.ts
@@ -23,7 +23,8 @@ export type InjectableTool<
Params extends z.ZodObject,
OutputSchema extends z.ZodObject,
Injected extends Partial> = {},
-> = Tool & {
+ Resolution = never,
+> = Tool & {
/**
* Optionally injects static parameter values into the tool's
* execute function and removes them from the parameter schema.
@@ -38,15 +39,18 @@ export function injectableTool<
Params extends z.ZodObject,
OutputSchema extends z.ZodObject,
Injected extends Partial>,
+ Resolution = never,
>({
description,
annotations,
parameters,
outputSchema,
hidden,
+ policy,
inject,
execute,
-}: InjectableTool) {
+ formatResult,
+}: InjectableTool) {
// If all injected parameters are undefined, return the original tool
if (!inject || Object.values(inject).every((value) => value === undefined)) {
return tool({
@@ -55,7 +59,9 @@ export function injectableTool<
parameters,
outputSchema,
hidden,
+ policy,
execute,
+ formatResult,
});
}
@@ -71,9 +77,10 @@ export function injectableTool<
// Wrapper that merges injected values with provided args
const executeWithInjection = async (
- args: z.infer
+ args: z.infer,
+ ...resolution: [Resolution] extends [never] ? [] : [Resolution]
) => {
- return execute({ ...args, ...inject } as z.infer);
+ return execute({ ...args, ...inject } as z.infer, ...resolution);
};
return tool({
@@ -82,7 +89,17 @@ export function injectableTool<
parameters: cleanParametersSchema,
outputSchema,
hidden,
+ // A policy decides on the same arguments the tool executes with, so the
+ // injected values are merged in before it sees them. Without this, a
+ // project-scoped server would ask a policy to reason about a project it
+ // was never told about.
+ policy: policy && {
+ ...policy,
+ resolve: (args, ctx) =>
+ policy.resolve({ ...args, ...inject } as z.infer, ctx),
+ },
execute: executeWithInjection,
+ formatResult,
});
}
diff --git a/packages/mcp-server-supabase/src/types.ts b/packages/mcp-server-supabase/src/types.ts
index 9260a72d..c89379a6 100644
--- a/packages/mcp-server-supabase/src/types.ts
+++ b/packages/mcp-server-supabase/src/types.ts
@@ -11,6 +11,15 @@ export const CURRENT_FEATURE_GROUPS = [
'storage',
] as const;
+/**
+ * Feature groups that need no platform implementation behind them.
+ *
+ * It lives beside the feature groups themselves rather than next to the server
+ * that consumes it, so a module reading feature facts never has to import the
+ * server and the cycle that used to create.
+ */
+export const PLATFORM_INDEPENDENT_FEATURES: FeatureGroup[] = ['docs'];
+
export const deprecatedFeatureGroupSchema = z.enum(['debug']);
export const currentFeatureGroupSchema = z.enum(CURRENT_FEATURE_GROUPS);
diff --git a/packages/mcp-server-supabase/src/util.ts b/packages/mcp-server-supabase/src/util.ts
index 9e3f499d..97312426 100644
--- a/packages/mcp-server-supabase/src/util.ts
+++ b/packages/mcp-server-supabase/src/util.ts
@@ -1,9 +1,9 @@
import { z } from 'zod/v4';
import type { SupabasePlatform } from './platform/types.js';
-import { PLATFORM_INDEPENDENT_FEATURES } from './server.js';
import {
currentFeatureGroupSchema,
featureGroupSchema,
+ PLATFORM_INDEPENDENT_FEATURES,
type FeatureGroup,
} from './types.js';
diff --git a/packages/mcp-server-supabase/test/cost-platform.ts b/packages/mcp-server-supabase/test/cost-platform.ts
new file mode 100644
index 00000000..e51aea0b
--- /dev/null
+++ b/packages/mcp-server-supabase/test/cost-platform.ts
@@ -0,0 +1,113 @@
+import type {
+ AccountOperations,
+ BranchingOperations,
+ CreationRate,
+ SupabasePlatform,
+} from '../src/platform/types.js';
+
+/**
+ * A platform whose every value is fixed, for tests that pin exact bytes or the
+ * order operations run in. Nothing here is random, so a result is reproducible
+ * across runs and comparable against a measured baseline.
+ */
+
+export const PROJECT_RATE: CreationRate = {
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+};
+
+export const BRANCH_RATE: CreationRate = {
+ amount: 0.01344,
+ currency: 'USD',
+ recurrence: 'hourly',
+};
+
+export const FIXED_PROJECT = {
+ id: 'fixed-project-ref',
+ ref: 'fixed-project-ref',
+ organization_id: 'fixed-org',
+ organization_slug: 'fixed-org',
+ name: 'Fixture Project',
+ status: 'UNKNOWN',
+ created_at: '2026-01-01T00:00:00.000Z',
+ region: 'us-east-1',
+};
+
+export const FIXED_BRANCH = {
+ id: 'fixed-branch',
+ name: 'develop',
+ project_ref: 'fixed-branch-ref',
+ parent_project_ref: 'fixed-project-ref',
+ is_default: false,
+ persistent: false,
+ status: 'CREATING_PROJECT' as const,
+ created_at: '2026-01-01T00:00:00.000Z',
+ updated_at: '2026-01-01T00:00:00.000Z',
+};
+
+export type CostPlatform = {
+ platform: SupabasePlatform;
+ /** Rate reads and creations, in the order they happened. */
+ calls: string[];
+ /** Rates the next reads return, one per read, before the default applies. */
+ projectRates: CreationRate[];
+ branchRates: CreationRate[];
+};
+
+export function createCostPlatform(): CostPlatform {
+ const calls: string[] = [];
+ const projectRates: CreationRate[] = [];
+ const branchRates: CreationRate[] = [];
+
+ const account: AccountOperations = {
+ async listOrganizations() {
+ return [{ id: 'fixed-org', slug: 'fixed-org', name: 'Fixture Org' }];
+ },
+ async getOrganization() {
+ return {
+ id: 'fixed-org',
+ name: 'Fixture Org',
+ plan: 'pro',
+ allowed_release_channels: ['ga'],
+ opt_in_tags: [],
+ };
+ },
+ async listProjects() {
+ return [FIXED_PROJECT];
+ },
+ async getProject() {
+ return FIXED_PROJECT;
+ },
+ async createProject() {
+ calls.push('create_project');
+ return FIXED_PROJECT;
+ },
+ async pauseProject() {},
+ async restoreProject() {},
+ async getProjectCreationRate() {
+ calls.push('read_project_rate');
+ return projectRates.shift() ?? PROJECT_RATE;
+ },
+ };
+
+ const branching: BranchingOperations = {
+ async listBranches() {
+ return [];
+ },
+ async createBranch() {
+ calls.push('create_branch');
+ return FIXED_BRANCH;
+ },
+ async deleteBranch() {},
+ async mergeBranch() {},
+ async resetBranch() {},
+ async rebaseBranch() {},
+ async getBranchCreationRate() {
+ calls.push('read_branch_rate');
+ return branchRates.shift() ?? BRANCH_RATE;
+ },
+ };
+
+ return { platform: { account, branching }, calls, projectRates, branchRates };
+}
diff --git a/packages/mcp-server-supabase/test/mocks.ts b/packages/mcp-server-supabase/test/mocks.ts
index 79ee03a3..fdd55d21 100644
--- a/packages/mcp-server-supabase/test/mocks.ts
+++ b/packages/mcp-server-supabase/test/mocks.ts
@@ -15,6 +15,7 @@ import {
} from '../src/content-api/graphql.js';
import { getDeploymentId, getPathPrefix } from '../src/edge-function.js';
import type { components } from '../src/management-api/types.js';
+import type { CreationRate } from '../src/platform/types.js';
const { version } = packageJson;
@@ -85,6 +86,30 @@ export const mockOrgs = new Map();
export const mockProjects = new Map();
export const mockBranches = new Map();
+/**
+ * Rates the mock Management API reports. They are the values Billing's addon
+ * catalog returns today, and they live here rather than in the package because
+ * the package holds no price of its own.
+ */
+export const MOCK_PROJECT_CREATION_RATE: CreationRate = {
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+};
+export const MOCK_BRANCH_CREATION_RATE: CreationRate = {
+ amount: 0.01344,
+ currency: 'USD',
+ recurrence: 'hourly',
+};
+
+/**
+ * Authoritative creation rates the next reads return, one per read, before the
+ * mock falls back to the rate Billing would compute. A test queues entries to
+ * reproduce a rate that moves between the proposal and the creation call.
+ */
+export const queuedProjectCreationRates: CreationRate[] = [];
+export const queuedBranchCreationRates: CreationRate[] = [];
+
export const mockContentApiSchemaLoadCount = { value: 0 };
export const mockContentApi = [
@@ -266,6 +291,84 @@ export const mockManagementApi = [
return HttpResponse.json(organization);
}),
+ /**
+ * Authoritative rate for the next project in an organization
+ */
+ http.get(
+ `${API_URL}/v2/organizations/:slug/project-creation-rate`,
+ ({ params }) => {
+ // Slug-strict, like the route: v2 paths take a slug, and nothing else
+ // resolves. A UUID-shaped identifier is not found here, exactly as it
+ // would not be found in production.
+ const organization = Array.from(mockOrgs.values()).find(
+ (org) => org.slug === params.slug
+ );
+
+ if (!organization) {
+ return HttpResponse.json(
+ { message: 'Organization not found' },
+ { status: 404 }
+ );
+ }
+
+ const activeProjects = Array.from(mockProjects.values()).filter(
+ (project) =>
+ project.organization_id === organization.id &&
+ !['INACTIVE', 'GOING_DOWN', 'REMOVED'].includes(project.status)
+ );
+
+ // Billing absorbs the first active project of a paid plan into the
+ // organization's compute credits, and a free plan is never charged.
+ const billable =
+ organization.plan !== 'free' && activeProjects.length > 0;
+ const rate =
+ queuedProjectCreationRates.shift() ??
+ (billable
+ ? MOCK_PROJECT_CREATION_RATE
+ : { ...MOCK_PROJECT_CREATION_RATE, amount: 0 });
+
+ return HttpResponse.json({
+ data: {
+ type: 'project_creation_rate',
+ attributes: {
+ ...rate,
+ organization_slug: organization.slug,
+ plan_id: organization.plan,
+ active_project_count: activeProjects.length,
+ },
+ },
+ });
+ }
+ ),
+
+ /**
+ * Authoritative rate for one new branch of a project
+ */
+ http.get(`${API_URL}/v2/projects/:ref/branch-creation-rate`, ({ params }) => {
+ const project = mockProjects.get(params.ref as string);
+
+ if (!project) {
+ return HttpResponse.json(
+ { message: 'Project not found' },
+ { status: 404 }
+ );
+ }
+
+ const rate = queuedBranchCreationRates.shift() ?? MOCK_BRANCH_CREATION_RATE;
+
+ return HttpResponse.json({
+ data: {
+ type: 'branch_creation_rate',
+ attributes: {
+ ...rate,
+ organization_slug: project.organization_slug,
+ plan_id: 'pro',
+ parent_project_ref: project.ref,
+ },
+ },
+ });
+ }),
+
/**
* Get the API keys for a project
*/
@@ -939,6 +1042,8 @@ export function setupMockApis(): SetupServer {
mockOrgs.clear();
mockProjects.clear();
mockBranches.clear();
+ queuedProjectCreationRates.length = 0;
+ queuedBranchCreationRates.length = 0;
mockContentApiSchemaLoadCount.value = 0;
const mockServer = setupServer(...mockContentApi, ...mockManagementApi);
@@ -1034,7 +1139,11 @@ export class MockOrganization {
constructor(options: MockOrganizationOptions) {
this.id = nanoid();
- this.slug = nanoid();
+ // The Management API reports an organization's slug under both names: v1
+ // marks `id` a deprecated alias of `slug`, kept for backwards
+ // compatibility. Mirroring that here keeps a test honest about what a
+ // client actually holds in `organization_id`.
+ this.slug = this.id;
this.name = options.name;
this.plan = options.plan;
this.allowed_release_channels = options.allowed_release_channels;
diff --git a/packages/mcp-server-supabase/test/stdio.integration.ts b/packages/mcp-server-supabase/test/stdio.integration.ts
index 2aaf0bb6..56ea0d38 100644
--- a/packages/mcp-server-supabase/test/stdio.integration.ts
+++ b/packages/mcp-server-supabase/test/stdio.integration.ts
@@ -329,6 +329,19 @@ describe('stdio', () => {
},
]);
expect(contentApiStub.hits.length).toBeGreaterThan(0);
+
+ // Deprecated stdio stays legacy-only: nothing injects form delivery
+ // here, so the paid tools keep the confirmation token they always
+ // required and advertise no structured output.
+ const createProject = tools.find(
+ (tool) => tool.name === 'create_project'
+ );
+ const createBranch = tools.find((tool) => tool.name === 'create_branch');
+
+ for (const tool of [createProject, createBranch]) {
+ expect(tool?.inputSchema.required).toContain('confirm_cost_id');
+ expect(tool?.outputSchema).toBeUndefined();
+ }
} finally {
await client.close();
}
diff --git a/scripts/fixtures/packed-platform-consumer/modern-call.mjs b/scripts/fixtures/packed-platform-consumer/modern-call.mjs
index 85cff15d..6923ec3f 100644
--- a/scripts/fixtures/packed-platform-consumer/modern-call.mjs
+++ b/scripts/fixtures/packed-platform-consumer/modern-call.mjs
@@ -2,18 +2,30 @@ import {
Client,
StreamableHTTPClientTransport,
} from '@modelcontextprotocol/client';
+import * as packageExports from '@supabase/mcp-server-supabase';
import { createSupabaseMcpHandler } from '@supabase/mcp-server-supabase';
// https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
const MODERN_PROTOCOL_VERSION = '2026-07-28';
-// Stubbed `account` operations only run on a tool call. The account feature
-// registers real zod-built schemas, and the tools/list checks below verify
-// that create_project keeps its required properties with Platform's zod pin.
-// `docs` stays out: its tool description lazily calls supabase.com, and this
-// exchange must never leave the process.
+// Stubbed `account` operations, real where this exchange needs them: the
+// account feature registers real zod-built schemas, and the checks below drive
+// a confirmed creation through them with Platform's zod pin. `docs` stays out:
+// its tool description lazily calls supabase.com, and this exchange must never
+// leave the process.
const notImplemented = () => Promise.reject(new Error('not implemented'));
+const FIXED_PROJECT = {
+ id: 'packed-fixture-ref',
+ ref: 'packed-fixture-ref',
+ organization_id: 'packed-fixture-org',
+ organization_slug: 'packed-fixture-org',
+ name: 'Packed Fixture',
+ status: 'UNKNOWN',
+ created_at: '2026-01-01T00:00:00.000Z',
+ region: 'us-east-1',
+};
+
const handler = createSupabaseMcpHandler({
platform: {
account: {
@@ -21,12 +33,23 @@ const handler = createSupabaseMcpHandler({
getOrganization: notImplemented,
listProjects: notImplemented,
getProject: notImplemented,
- createProject: notImplemented,
+ createProject: async () => FIXED_PROJECT,
pauseProject: notImplemented,
restoreProject: notImplemented,
+ getProjectCreationRate: async () => ({
+ amount: 10,
+ currency: 'USD',
+ recurrence: 'monthly',
+ }),
},
},
features: ['account'],
+ // The dependencies a hosted modern route injects.
+ elicitation: {
+ actorId: 'packed-fixture-approver',
+ stateKey: 'a-platform-managed-state-key-long-enough',
+ formDeliveryAvailable: true,
+ },
});
const transport = new StreamableHTTPClientTransport(
@@ -39,9 +62,29 @@ const transport = new StreamableHTTPClientTransport(
const client = new Client(
{ name: 'packed-platform-consumer-fixture', version: '0.0.0' },
- { versionNegotiation: { mode: { pin: MODERN_PROTOCOL_VERSION } } }
+ {
+ capabilities: { elicitation: {} },
+ versionNegotiation: { mode: { pin: MODERN_PROTOCOL_VERSION } },
+ }
);
+let elicitations = 0;
+client.setRequestHandler('elicitation/create', async (request) => {
+ elicitations += 1;
+ const { message, requestedSchema } = request.params;
+
+ if (typeof message !== 'string' || !message.includes('10 USD')) {
+ throw new Error(`confirmation message lost its rate: ${message}`);
+ }
+ if (Object.keys(requestedSchema?.properties ?? {}).length > 0) {
+ throw new Error(
+ `confirmation asked for properties: ${JSON.stringify(requestedSchema)}`
+ );
+ }
+
+ return { action: 'accept' };
+});
+
await client.connect(transport);
const { tools } = await client.listTools();
@@ -78,6 +121,45 @@ if (missingCreateProjectProperties.length > 0) {
);
}
+// The legacy confirmation surface is gone for a capable client, and the
+// confirmation happens inside the creation call instead.
+if (tools.some((tool) => tool.name === 'confirm_cost')) {
+ throw new Error('a form-capable client was offered confirm_cost');
+}
+if (createProjectProperties.includes('confirm_cost_id')) {
+ throw new Error('a form-capable client was offered confirm_cost_id');
+}
+
+const created = await client.callTool({
+ name: 'create_project',
+ arguments: {
+ name: 'Packed Fixture',
+ region: 'us-east-1',
+ organization_id: 'packed-fixture-org',
+ },
+});
+
+if (elicitations !== 1) {
+ throw new Error(`expected exactly one confirmation, saw ${elicitations}`);
+}
+if (created.structuredContent?.ref !== FIXED_PROJECT.ref) {
+ throw new Error(
+ `confirmed creation lost its business output: ${JSON.stringify(created)}`
+ );
+}
+
+// The runtime, its state, its codec, and the policy stay private: a hosted
+// consumer injects dependencies and never reaches inside.
+const leaked = Object.keys(packageExports).filter((name) =>
+ /elicitation|runtime|continuation|codec|policy|interaction|replay|cost/i.test(
+ name
+ )
+);
+
+if (leaked.length > 0) {
+ throw new Error(`package entry point leaked private symbols: ${leaked}`);
+}
+
await client.close();
await handler.close();
diff --git a/scripts/fixtures/packed-platform-consumer/types-check.ts b/scripts/fixtures/packed-platform-consumer/types-check.ts
index 5e64e7c3..7b6307a3 100644
--- a/scripts/fixtures/packed-platform-consumer/types-check.ts
+++ b/scripts/fixtures/packed-platform-consumer/types-check.ts
@@ -1,12 +1,35 @@
import {
createSupabaseMcpHandler,
+ type SupabaseElicitationOptions,
type SupabaseMcpServerOptions,
} from '@supabase/mcp-server-supabase';
-// A stubbed `account` platform, whose seven operations only ever run on a
-// tool call and so can reject here. It buys the thing that matters: asking
-// for the `account` feature group makes the server register real tools, so
-// the typecheck covers the zod-backed tool surface rather than an empty one.
+// The supported hosted-injection surface, pinned exactly. A private runtime
+// knob reaching the packed declarations (a clock, a correlation-id seam, a
+// configurable lifetime) fails here rather than in a hosted deployment.
+type HostedInjectionKeys =
+ | 'stateKey'
+ | 'actorId'
+ | 'formDeliveryAvailable'
+ | 'optOut'
+ | 'gate';
+
+type Exactly = [Actual] extends [Expected]
+ ? [Expected] extends [Actual]
+ ? true
+ : { unexpected: Exclude }
+ : { missing: Exclude };
+
+const elicitationKeysArePinned: Exactly<
+ keyof SupabaseElicitationOptions,
+ HostedInjectionKeys
+> = true;
+void elicitationKeysArePinned;
+
+// A stubbed `account` platform, whose operations only ever run on a tool call
+// and so can reject here. It buys the thing that matters: asking for the
+// `account` feature group makes the server register real tools, so the
+// typecheck covers the zod-backed tool surface rather than an empty one.
const account: SupabaseMcpServerOptions['platform']['account'] = {
listOrganizations: () => Promise.reject(new Error('not implemented')),
getOrganization: () => Promise.reject(new Error('not implemented')),
@@ -15,11 +38,28 @@ const account: SupabaseMcpServerOptions['platform']['account'] = {
createProject: () => Promise.reject(new Error('not implemented')),
pauseProject: () => Promise.reject(new Error('not implemented')),
restoreProject: () => Promise.reject(new Error('not implemented')),
+ getProjectCreationRate: () => Promise.reject(new Error('not implemented')),
};
const options: SupabaseMcpServerOptions = {
platform: { account },
features: ['account'],
+ // Exactly what a hosted modern route injects. If any of it stops being
+ // consumable from the packed artifact, this typecheck fails rather than a
+ // deployment.
+ elicitation: {
+ actorId: 'auth-grant-id',
+ stateKey: 'a-platform-managed-state-key-long-enough',
+ formDeliveryAvailable: true,
+ optOut: false,
+ gate: () => null,
+ },
+ onToolPolicyCall: ({ name, decision, telemetry }) => {
+ void name;
+ void decision;
+ void telemetry.interactionId;
+ void telemetry.outcome;
+ },
};
const handler = createSupabaseMcpHandler(options);