Skip to content

Platform API

The same MCP tool surface is available over HTTP:

MethodPathPurpose
GET, POST, DELETE/mcpStreamable HTTP MCP session

Send Authorization: Bearer {platform_api_key} so requests run as your account. Use this when your assistant or automation can’t use stdio MCP but can call HTTPS.

See the Regions concept guide for an overview.

GET /v1/regions

Public — no API key required.

{ "regions": ["us-east-1", "us-west-2"] }
POST /init
Authorization: Bearer {api_key}
{
"name": "my-app",
"region": "us-west-2"
}
FieldRequiredNotes
nameYesApp display name; also used to derive the subdomain.
regionNoOne of the regions returned by GET /v1/regions. Defaults to the platform default if omitted.

Returns { app_id, api_base_url, region, ... }.

POST /v1/apps/{app_id}/move
Authorization: Bearer {api_key}
{ "dest_region": "us-east-1" }

Returns { migration_id, status: "queued" }. The app stays available for reads during the move; writes pause briefly during the cutover and resume automatically when the move completes.

GET /v1/apps/{app_id}/migrations/{migration_id}
Authorization: Bearer {api_key}

Returns the current step, source and destination regions, and timing.

MethodPathPurpose
GET/llms.txtPlain-text guidance for LLM agents

Provides quick start patterns, common patterns, error shape, and response metadata.

Control whether anonymous (unauthenticated) requests can reach the data API and realtime WebSocket. See Access modes for the conceptual overview.

MethodPathPurpose
PATCH/v1/{app_id}/config/access-modeToggle between public and authenticated
POST/v1/{app_id}/secureSet access_mode = "authenticated" and create user-isolation RLS policies in one call
PATCH /v1/{app_id}/config/access-mode
Authorization: Bearer {token}
{ "access_mode": "authenticated" }
POST /v1/{app_id}/secure
Authorization: Bearer {token}
{
"tables": [
{ "table_name": "posts", "user_column": "author_id" },
{ "table_name": "comments", "user_column": "user_id", "public_read_column": "is_published" }
]
}

Pass an empty body or omit tables to flip access mode only. Response includes tables_secured and a table_errors array — failures on individual tables don’t roll back the whole call.

Control whether other Butterbase users can discover and clone your app as a template. This is separate from access mode, which controls whether anonymous requests reach the data API. An app can be visibility="public" (template-shareable) and access_mode="authenticated" (no anonymous data reads) at the same time.

visibility defaults to "private". Set it to "public" to make the app appear in the dashboard Templates browser and GET /v1/templates. Combine with listed: false to keep the app clonable by direct id but hidden from browse listings.

MethodPathPurpose
PATCH/v1/{app_id}/config/visibilitySet visibility and optionally listed
PATCH /v1/{app_id}/config/visibility
Authorization: Bearer {token}
{ "visibility": "public" }

Response:

{ "message": "Visibility updated to \"public\"", "app_id": "{app_id}", "visibility": "public", "listed": true }

listed is optional. When true (the default for public apps), the app will appear in the upcoming public templates browser. Pass "listed": false to keep the app accessible by direct link while hiding it from the browse list.

visibility accepts "public" or "private". Defaults to "private".

GET /v1/{app_id}/config returns visibility and listed alongside the other app settings.

Clone a public app to get a copy of its repo snapshot as a starting point for your own project.

MethodPathPurpose
GET/v1/templatesBrowse public app templates (anonymous)
GET/v1/templates/{app_id}Detail for a single public app template
POST/v1/templates/{source_app_id}/cloneStart a clone of a public app. Returns { job_id }
GET/v1/clone-jobs/{job_id}Status of a clone job
POST/v1/clone-jobs/{job_id}/retryRetry a failed clone job

GET /v1/templates returns the catalog of public, listed apps across all regions. No authentication required. Supports q (name prefix), region, sort=recent|popular, limit (max 50), offset. Response: { items: [{ app_id, name, owner_display_name, region, created_at, fork_count, has_repo, schema_summary: { table_count, function_count } }], total, limit, offset }.

GET /v1/templates/{app_id} returns full detail for a single template, including tables, functions, and forks_sample (5 most recent clones). Private or unlisted apps return 404 (no existence leak).

POST /v1/templates/{source_app_id}/clone creates a new app owned by the caller and copies the source into it. Body: { name?: string, region?: string } — region defaults to the source’s region. Source must be public; private apps return 404 (no existence leak). Returns { job_id, status: "pending" }.

Poll GET /v1/clone-jobs/{job_id} until status is "completed" (response includes the new dest_app_id) or "failed" (response includes error_message). Failed jobs can be retried via POST /v1/clone-jobs/{job_id}/retry.

What the clone copies: the source app’s database schema (tables, columns, indexes), row-level security policies, function code, repo files (latest snapshot at clone time), non-secret configuration (storage settings, allowed origins, OAuth provider and URLs, AI model defaults), and any rows in tables the template author marked as seed data.

What it does not copy: end-user accounts and sessions, OAuth client credentials, function environment variables, bring-your-own-key (BYOK) AI provider keys, custom domains, billing, function invocation history, and audit logs. The clone owner must set these up themselves. See What a clone copies for the full breakdown.

Push a codebase to your app as a content-addressed snapshot. Useful as a cross-device backup / source of truth for your app’s files. Snapshots are content-addressed: re-pushing an unchanged file does not re-upload it. The last 5 snapshots are retained.

Repo content is stored in your app’s object storage under a reserved prefix and is not counted as a normal storage object — uploads go through a separate two-phase flow.

Push has two phases:

  1. prepare — send the manifest (file paths + their sha256 + sizes). Server validates and returns presigned PUT URLs for any blobs it doesn’t already have.
  2. commit — after uploading the listed blobs to S3 with the returned URLs, send the manifest again. Server verifies every blob landed at the declared size, writes the manifest, and points latest at the new snapshot.
MethodPathPurpose
POST/v1/{app_id}/repo/snapshots/prepareValidate a manifest; receive presigned PUTs for missing blobs
POST/v1/{app_id}/repo/snapshots/commitFinalize the snapshot after all blobs are uploaded
GET/v1/{app_id}/repo/snapshotsList snapshot history (newest first)
GET/v1/{app_id}/repo/snapshots/latestFetch the current snapshot manifest
GET/v1/{app_id}/repo/snapshots/{snapshot_id}Fetch a specific snapshot’s manifest
GET/v1/{app_id}/repo/blobs/{sha256}Receive a presigned GET URL for a single blob
POST/v1/{app_id}/repo/blobs/batchPresign multiple blob download URLs in a single call
DELETE/v1/{app_id}/repoWipe the entire repo

Reads (GET) on a public app are anonymous. On a private app, only the owner can read or write.

{
"files": [
{ "path": "src/index.ts", "sha256": "<64 hex>", "size": 1234 }
],
"message": "optional push message"
}

Paths must be relative, ASCII-safe, contain no .. segments, no leading /, no backslashes, no null bytes, and be at most 4 KB. Hard caps: 10 MB per file, 100 MB per snapshot.

POST /v1/{app_id}/repo/snapshots/prepare
Authorization: Bearer {token}
{ "files": [ { "path": "a.txt", "sha256": "...", "size": 5 } ] }

Response:

{
"snapshot_id": "<64 hex>",
"total_bytes": 5,
"file_count": 1,
"missing_blobs": [ { "sha256": "...", "uploadUrl": "https://..." } ]
}

Upload each missing_blob.uploadUrl with PUT. The presigned URL expires after 10 minutes.

POST /v1/{app_id}/repo/snapshots/commit
Authorization: Bearer {token}
{ "manifest": { "files": [ ... ], "message": "..." } }

If any blob is still missing or its uploaded size doesn’t match the manifest, the response is 409 with details.missing_shas and details.size_mismatches. Re-upload the listed blobs and re-call commit.

GET /v1/{app_id}/repo/snapshots/latest

Returns { snapshot_id, manifest }. For each file in the manifest, request GET /v1/{app_id}/repo/blobs/{sha256} to receive a presigned GET URL (1 hour expiry), then fetch.

DELETE /v1/{app_id}/repo removes every snapshot, blob, and the latest pointer. Cannot be undone.

GET /v1/{app_id}/repo/snapshots returns { snapshots: [{ snapshot_id, created_at }] } sorted newest-first. Visibility rules are the same as latest: anonymous on a public app, owner-only on a private one (returns 404 to non-owners — no existence leak).

POST /v1/{app_id}/repo/blobs/batch accepts { shas: string[] } (max 1000 entries) and returns { blobs: [{ sha256, size, downloadUrl, expiresIn }] }. Missing or pruned shas are omitted from the response, so the response length can be less than the request length. Visibility rules match the single-blob endpoint: anonymous on a public app, owner-only on a private one.

This is useful when you have a manifest with many files and want to fetch all blobs without N round trips. The MCP manage_repo pull_latest action uses this internally.

When subdomain routing is enabled, each app has a subdomain derived from its name. Traffic to https://{subdomain}.{base_domain} resolves the app from the Host header, so you omit {app_id} from paths.

Subdomain pathEquivalent purpose
/data/{table}Data API CRUD
/fn/{function_name}Invoke function
/auth/signup, /auth/login, …End-user auth
/storage/upload, /storage/objects, …File storage
/schema, /schema/apply, /migrationsSchema management

MCP tool: submit_suggestion

HTTP:

POST /suggestions
Authorization: Bearer {api_key}
{
"category": "feature_request",
"description": "Add support for GraphQL subscriptions",
"severity": "medium",
"source": "human_prompted"
}
FieldRequiredValues
categoryYesbug_report, feature_request, improvement, documentation
descriptionYesDescription text
severityNolow, medium, high, critical
affected_toolNoTool name if applicable
proposed_solutionNoSuggested fix
sourceNoagent or human_prompted
MethodPathPurpose
GET/healthLiveness check
GET/health/readyReadiness check (database connectivity)

All errors include structured objects:

{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Table 'nonexistent' does not exist",
"remediation": "Check the table name and ensure it exists in your schema",
"documentation_url": "https://docs.butterbase.ai/api-reference/data-api"
}
}

Follow the remediation field before retrying.

Sensitive routes (especially auth) have strict per-route rate limits. Other routes may have additional limits depending on deployment configuration.