
Volcengine Db Supabase
- 25 installs
- 16 repo stars
- Updated August 3, 2026
- volcengine/volcengine-skills
Helps with ai & agent building tasks.
About
volcengine-db-supabase is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- volcengine-db-supabase
- AI & Agent Building
- AI-coding skill
Volcengine Db Supabase by the numbers
- 25 all-time installs (skills.sh)
- Ranked #9,740 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/volcengine/volcengine-skills --skill volcengine-db-supabaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 16 |
| Last updated | August 3, 2026 |
| Repository | volcengine/volcengine-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Volcengine AIDAP Database Skill
AIDAP refers to Volcengine's AI 原生 BaaS 平台 Supabase 版 product. This skill manages its deployment-relevant database workspace capabilities and wires them into applications. The stable deploy-facing AIDAP engine choices are supabase and postgresql; resolve current CreateWorkspace EngineType / EngineVersion enums from `references/tool-reference.md` before creating a workspace. The control plane should use ve aidap directly whenever the action exists. For old-skill data-plane capabilities not exposed by ve, use scripts/supabase_dataplane.py only for Supabase-compatible workspace APIs.
Boundaries
- Supported by
ve aidap: workspace, branch, compute, database, DB account, endpoint, API key, ACL, schema diff, start/stop, and deletion operations. - AIDAP deploy engine choices are
supabaseandpostgresql. Preserve the user's selected engine instead of treating Supabase as an RDS PostgreSQL provider. - Not covered by current
veCLI: enterprise real-name verification (account_verify/GetVerifyInfo/2018-01-01). Usescripts/aidap_bootstrap.pyfor that check when needed. scripts/aidap_bootstrap.pysigns non-public API requests fromVOLCENGINE_ACCESS_KEYandVOLCENGINE_SECRET_KEY; it does not reuseve logincached credentials.- Not covered by current
veCLI: SQL execution, migration application, Supabase Edge Function management, Supabase Storage bucket management, and TypeScript type generation. Usescripts/supabase_dataplane.pyfor those old-skill capabilities. scripts/supabase_dataplane.pyusesve aidaponly to resolve endpoint, default branch, and API keys. The data-plane calls use the Supabase service-role key inapikeyandAuthorizationheaders, not Volcengine AK/SK.- Service activation is a console flow. If the service is not enabled, direct the user to
https://console.volcengine.com/iam/service/attach_role/?ServiceName=aidap.
Initial Checks
1. Verify authentication with ve sts GetCallerIdentity. 2. Confirm AIDAP support in the installed CLI:
ve aidap --help3. If AK/SK environment variables are available, check enterprise real-name status when creating a workspace or troubleshooting creation failure:
python3 scripts/aidap_bootstrap.py get-verify-infoThe account is enterprise verified only when the response has IsVerified=true and IdentityType="enterprise". The script also emits verification.enterprise_verified from those two fields.
Workspace Bootstrap
When the user has no workspace:
1. Ensure the AIDAP service is enabled. If not, ask the user to open:
https://console.volcengine.com/iam/service/attach_role/?ServiceName=aidap2. Check enterprise verification with the bootstrap script when AK/SK environment variables are available. 3. Create the workspace with ve aidap CreateWorkspace, using the selected AIDAP engine and the current official enum mapping from `references/tool-reference.md`.
Minimal explicit-network body shape:
ve aidap CreateWorkspace --body '{
"WorkspaceName": "demo-supabase",
"EngineType": "<current EngineType for database_engine>",
"EngineVersion": "<current EngineVersion for database_engine>",
"BranchSettings": {
"BranchName": "main",
"DatabaseName": "postgres"
},
"ComputeSettings": {
"AutoScalingLimitMinCU": 0.25,
"AutoScalingLimitMaxCU": 1,
"SuspendTimeoutSeconds": 300
},
"NetworkSettings": {
"VpcId": "vpc-xxxx",
"SubnetId": "subnet-xxxx",
"SharedPublicNetwork": false
},
"WorkspaceSettings": {
"DeletionProtection": "Disabled",
"PublicConnection": "Disabled"
},
"WorkspaceTags": [
{"Key": "publish-by", "Value": "deploy-skill"}
]
}'Use a subnet in the same region as the workspace. For database_engine=postgresql, prefer an explicit VpcId and SubnetId from an existing Available VPC/subnet, such as the account's default VPC/subnet. Do not start with shared public/shared-network-only creation and then retry into explicit networking; the explicit network path is the verified low-friction path for PostgreSQL workspaces.
Do not invent or hard-code stale EngineType / EngineVersion values. Check the current API enum table in `references/tool-reference.md`, then refresh it from the official CreateWorkspace documentation or CLI/API evidence before live creation if the table may be stale.
After CreateWorkspace, verify readiness in dependency order:
1. Poll DescribeWorkspaceDetail until WorkspaceStatus=Running. 2. Poll DescribeDefaultBranch until the branch has BranchStatus=Ready; keep its BranchId. 3. Run DescribeComputes for that branch and keep the Primary database compute's ComputeId when ComputeStatus=Active.
For PostgreSQL workspaces, pass both BranchId and ComputeId to endpoint and database connection queries. A verified DescribeWorkspaceEndpoint call with only BranchId returned InvalidParameter: 参数ComputeId值无效.
Common Commands
Use ve aidap <Action> --help before writing command arguments; AIDAP is evolving and the CLI help is the local source of truth.
ve aidap DescribeWorkspaces --Limit 20
ve aidap DescribeWorkspaceDetail --WorkspaceId ws-xxxx
ve aidap DescribeDefaultBranch --WorkspaceId ws-xxxx
ve aidap DescribeBranches --WorkspaceId ws-xxxx
ve aidap DescribeComputes --WorkspaceId ws-xxxx --BranchId br-xxxx
ve aidap CreateBranch --WorkspaceId ws-xxxx --BranchSettings.Name dev
ve aidap DescribeWorkspaceEndpoint --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx
ve aidap DescribeAPIKeys --WorkspaceId ws-xxxx --BranchId br-xxxx
ve aidap DescribeDBAccounts --WorkspaceId ws-xxxx --BranchId br-xxxx
ve aidap CreateDBAccount --WorkspaceId ws-xxxx --BranchId br-xxxx --AccountName app --AccountPassword '<secret>'
ve aidap CreateDatabase --WorkspaceId ws-xxxx --BranchId br-xxxx --DatabaseName appdb --DatabaseOwner app
ve aidap DescribeDBAccountConnection --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx --DatabaseName appdb --AccountName app
ve aidap DescribeSupabaseDeployEnvVars --WorkspaceId ws-xxxx --BranchId br-xxxxBranch-scoped actions must pass the explicit BranchId; do not rely on an implicit default branch for endpoint, API key, DB account, database, connection, or deploy-env-var operations. DescribeWorkspaceEndpoint without BranchId has been observed to fail with InvalidParameter. For PostgreSQL endpoint and account connection queries, also pass the resolved primary ComputeId.
If CreateDBAccount or CreateDatabase reports PrimaryComputeNotFound, first compare DescribeDefaultBranch, DescribeBranches, and DescribeComputes for the same workspace and branch. If the branch is the default branch and DescribeComputes shows a Primary database compute in Active state, stop guessing branch IDs and record the case as an AIDAP control-plane inconsistency. Use the PostgreSQL fallback in `references/deploy-provider.md` only when a credential-bearing admin POSTGRES_URL is available and the user accepts using it.
Never print passwords, API keys, JWT secrets, service-role keys, or connection strings containing credentials in final answers. Write DATABASE_URL into a local env file with mode 600, then verify it with psql (select 1 or a table-list query). Summarize the host/port, resource IDs, verification result, and credential file path, not the full connection string.
CreateAccessControlList currently has fragile CLI array parameter handling and has returned InvalidParameterFormat for common array forms. Do not tell the user that an ACL has been tightened until you verify the effective AllowHost from DescribeDBAccountConnection. If AllowHost still includes broad ranges such as 0.0.0.0/0 or ::/0, warn clearly and recommend tightening in the console or with a separately verified API call.
Data-Plane Commands
Use scripts/supabase_dataplane.py for old-skill capabilities that ve aidap does not expose. The script accepts either --workspace-id ws-... or --workspace-id br-...; when only a workspace is supplied it resolves the default branch with ve aidap DescribeDefaultBranch.
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select * from pg_tables limit 5"
python3 scripts/supabase_dataplane.py apply-migration --workspace-id ws-xxxx --name create_todos --query-file ./migration.sql
python3 scripts/supabase_dataplane.py generate-typescript-types --workspace-id ws-xxxx --schemas public
python3 scripts/supabase_dataplane.py deploy-edge-function --workspace-id ws-xxxx --function-name hello --source-file ./index.ts
python3 scripts/supabase_dataplane.py create-storage-bucket --workspace-id ws-xxxx --bucket-name uploads --publicSet READ_ONLY=true to block data-plane write actions. If SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are already set, the script can skip ve aidap key lookup.
Deploy Integration
When volcengine-deploy needs an AIDAP database, it passes database_product=aidap plus database_engine=supabase|postgresql. Read `references/deploy-provider.md` for the wiring loop and environment variables.
References
- CLI action map and bootstrap notes: `references/tool-reference.md`
- Application integration patterns: `references/app-integration-guide.md`
- Schema and RLS guidance: `references/schema-rls-guide.md`
- SQL playbook: `references/sql-playbook.md`
- Edge Function development: `references/edge-function-dev-guide.md`
- Deployment database-provider wiring: `references/deploy-provider.md`
Application Integration
Use this reference when the user needs to wire an application to a Volcengine Supabase workspace. It describes application-side patterns only; resource management should still go through ve aidap.
Collect Connection Values
Use the control plane to fetch endpoint and key metadata:
ve aidap DescribeComputes --WorkspaceId ws-xxxx --BranchId br-xxxx
ve aidap DescribeWorkspaceEndpoint --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx
ve aidap DescribeAPIKeys --WorkspaceId ws-xxxx --BranchId br-xxxx
ve aidap DescribeDBAccountConnection --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx --DatabaseName appdb --AccountName appFor PostgreSQL workspaces, resolve the primary database ComputeId first and include it in endpoint and account-connection reads. Calls with only BranchId can fail with InvalidParameter.
Map the returned values into runtime configuration:
SUPABASE_URL=<workspace-api-url>
SUPABASE_ANON_KEY=<anon-key>
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
DATABASE_URL=<postgres-connection-url>SUPABASE_SERVICE_ROLE_KEY and DATABASE_URL are backend secrets. Never expose them to frontend bundles, logs, or final summaries. If you need to hand off DATABASE_URL, write it to a local env file with mode 600 and report only the path.
TypeScript Client
npm install @supabase/supabase-jsimport { createClient } from "@supabase/supabase-js";
export function getSupabaseClient(userToken?: string) {
const url = process.env.SUPABASE_URL!;
const key = process.env.SUPABASE_ANON_KEY!;
return createClient(url, key, {
global: userToken ? { headers: { Authorization: `Bearer ${userToken}` } } : undefined,
auth: { autoRefreshToken: false, persistSession: false },
});
}Python Client
pip install supabaseimport os
from supabase import create_client
def get_supabase_client():
return create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_ANON_KEY"])Migrations and Direct SQL
Prefer the application's migration tool or a direct PostgreSQL client pointed at DATABASE_URL for project-owned migrations.
Examples:
umask 077
printf 'DATABASE_URL=%q\n' "$DATABASE_URL" > .aidap/ws-xxxx-postgres.env
psql "$DATABASE_URL" -f migrations/001_init.sql
psql "$DATABASE_URL" -Atc "select 1"
npx prisma migrate deploy
npx supabase db push --db-url "$DATABASE_URL"For compatibility with the imported old skill, this skill also provides the old REST data-plane actions:
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select 1"
python3 scripts/supabase_dataplane.py apply-migration --workspace-id ws-xxxx --name init --query-file migrations/001_init.sql
python3 scripts/supabase_dataplane.py generate-typescript-types --workspace-id ws-xxxx --schemas publicBefore running migrations, confirm the target workspace, branch, database, and account.
Storage and Edge Functions
The old skill managed Supabase Storage and Edge Functions through the Supabase REST API. Those actions are preserved in scripts/supabase_dataplane.py:
python3 scripts/supabase_dataplane.py create-storage-bucket --workspace-id ws-xxxx --bucket-name uploads --public
python3 scripts/supabase_dataplane.py list-storage-buckets --workspace-id ws-xxxx
python3 scripts/supabase_dataplane.py deploy-edge-function --workspace-id ws-xxxx --function-name hello --source-file ./index.ts
python3 scripts/supabase_dataplane.py list-edge-functions --workspace-id ws-xxxxFor application file operations, keep using the Supabase SDK with SUPABASE_URL and SUPABASE_ANON_KEY or backend-only service credentials.
AIDAP as a Deploy Database Provider
Use this reference from volcengine-deploy when the user selects database_product=aidap. AIDAP refers to Volcengine's AI 原生 BaaS 平台 Supabase 版 product; in deploy flows, use its database workspace surface as the managed database provider. AIDAP deploy-facing database engines are supabase and postgresql; resolve current CreateWorkspace EngineType / EngineVersion enums from `tool-reference.md` before creation.
Selection
Use AIDAP when:
- The user selects AIDAP as the database product.
- The user selects the Supabase or PostgreSQL engine in the AIDAP workspace flow.
- The deployment accepts AIDAP service prerequisites.
Prefer RDS when:
- The user selects RDS MySQL, RDS PostgreSQL, or RDS SQL Server.
- The deployment needs mature private VPC/IaC-oriented database operations.
- The user needs mature Terraform/IaC coverage for the database resource.
- The account cannot complete enterprise real-name verification or AIDAP service activation.
Provisioning Loop
1. Check ve sts GetCallerIdentity. 2. If ve aidap DescribeWorkspaces --Limit 1 indicates the service is not enabled, ask the user to complete:
https://console.volcengine.com/iam/service/attach_role/?ServiceName=aidap3. If AK/SK environment variables are available, run enterprise verification:
python3 skills/volcengine-db-supabase/scripts/aidap_bootstrap.py get-verify-infoTreat the account as enterprise verified only when verification.enterprise_verified=true, which requires IsVerified=true and IdentityType="enterprise".
4. Create or reuse the workspace with ve aidap, preserving database_engine=supabase|postgresql. For PostgreSQL, pass explicit VpcId and SubnetId; an existing Available default VPC/subnet is acceptable. 5. Wait for DescribeWorkspaceDetail to show WorkspaceStatus=Running, then DescribeDefaultBranch to show BranchStatus=Ready, then DescribeComputes to find the active primary database ComputeId. 6. Create or reuse an app DB account and database. Always pass the resolved BranchId to branch-scoped AIDAP actions. 7. Fetch endpoint/API key/DB connection information. For PostgreSQL, pass both BranchId and the primary ComputeId to DescribeWorkspaceEndpoint and DescribeDBAccountConnection. 8. Store DATABASE_URL in a local env file with mode 600, inject runtime variables into ECS systemd env files, VKE Secrets, or veFaaS env vars, and do not print the full URL. 9. Run migrations with the app's migration tool, direct PostgreSQL client, or the preserved supabase_dataplane.py apply-migration command when compatibility with the old skill is required. 10. Verify one database-backed behavior with psql (select 1 or a table-list query) or the app's health check. 11. Re-check DescribeDBAccountConnection.Result.AllowHost; if broad ranges such as 0.0.0.0/0 or ::/0 remain, warn that public access is not narrowed. CreateAccessControlList CLI array arguments have returned InvalidParameterFormat, so do not claim ACL tightening succeeded without this verification.
Control-Plane DB Fallback
If CreateDBAccount or CreateDatabase fails with PrimaryComputeNotFound, gather these read-only facts before changing IDs or retrying blindly:
ve aidap DescribeDefaultBranch --WorkspaceId ws-xxxx
ve aidap DescribeBranches --WorkspaceId ws-xxxx
ve aidap DescribeComputes --WorkspaceId ws-xxxx --BranchId br-xxxxWhen the target branch is the default branch and DescribeComputes reports a Primary database compute in Active state, treat the failure as AIDAP control-plane inconsistency. Use the admin POSTGRES_URL from deploy env vars as a fallback only after the user accepts using a credential-bearing database URL:
ve aidap DescribeSupabaseDeployEnvVars --WorkspaceId ws-xxxx --BranchId br-xxxx
psql "$POSTGRES_URL"For the fallback, create the app role and database through SQL. CREATE DATABASE appdb OWNER app_user can fail for a non-superuser admin connection with must be able to SET ROLE. Use admin-owned database creation, then grant the app role access:
CREATE ROLE app_user LOGIN PASSWORD '<secret>';
CREATE DATABASE appdb;
\connect appdb
GRANT CONNECT ON DATABASE appdb TO app_user;
GRANT USAGE, CREATE ON SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;Do not print or persist POSTGRES_URL, generated passwords, or SQL containing secrets.
Runtime Variables
Common variables:
DATABASE_URLSupabase-compatible engine values:
SUPABASE_URL
SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEYFor frontend frameworks, only expose public URL and anon key values through framework-specific public prefixes. Keep service-role keys and database URLs server-side.
When writing DATABASE_URL locally for deployment handoff, use a credential file outside source-controlled paths when possible, set mode 600, and report the file path instead of the URL value.
Old Skill Data-Plane Compatibility
When database_engine=supabase and volcengine-deploy or a user workflow expects old Supabase skill actions, use:
python3 skills/volcengine-db-supabase/scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select 1"
python3 skills/volcengine-db-supabase/scripts/supabase_dataplane.py apply-migration --workspace-id ws-xxxx --name deploy_migration --query-file ./migration.sql
python3 skills/volcengine-db-supabase/scripts/supabase_dataplane.py generate-typescript-types --workspace-id ws-xxxx --schemas publicDo not store service-role keys, generated connection URLs, or migration SQL containing secrets in .volcengine/created-resources.json.
Cleanup
Record CLI-created workspaces, branches, databases, and accounts in .volcengine/created-resources.json when volcengine-deploy creates them. Do not record secret values. Destructive cleanup must require explicit user confirmation.
Edge Function Development
Use this reference when the user needs Supabase Edge Function examples or old-skill-compatible deployment. Current ve aidap does not deploy Supabase Edge Functions; use the preserved data-plane command.
Basic Deno Function
Deno.serve(async (req) => {
const { pathname } = new URL(req.url);
if (pathname === "/health") {
return Response.json({ ok: true });
}
return new Response("not found", { status: 404 });
});Deploy from a file:
python3 scripts/supabase_dataplane.py deploy-edge-function --workspace-id ws-xxxx --function-name health --source-file ./index.tsDeploy a public endpoint without JWT verification:
python3 scripts/supabase_dataplane.py deploy-edge-function --workspace-id ws-xxxx --function-name webhook --source-file ./webhook.ts --no-verify-jwtCORS
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
return Response.json({ ok: true }, { headers: corsHeaders });
});Auth-Aware Database Access
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization") ?? "";
const client = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: authHeader } } },
);
const { data, error } = await client.from("posts").select("*").limit(20);
if (error) {
return Response.json({ error: error.message }, { status: 500 });
}
return Response.json({ data });
});Use anon-key clients when user identity and RLS should apply. Keep service-role keys server-side and only use them for trusted administrative logic.
Management Commands
python3 scripts/supabase_dataplane.py list-edge-functions --workspace-id ws-xxxx
python3 scripts/supabase_dataplane.py get-edge-function --workspace-id ws-xxxx --function-name health
python3 scripts/supabase_dataplane.py delete-edge-function --workspace-id ws-xxxx --function-name healthRuntime Notes
- Default runtime is
native-node20/v1. - Supported compatibility runtimes also include
native-python3.9/v1,native-python3.10/v1, andnative-python3.12/v1. - Node functions that export a default handler are wrapped with a generated
Deno.serveentrypoint, matching the old skill behavior. - Keep functions small and avoid long blocking work; Edge Function execution has time limits.
Schema and RLS Guidance
Use this reference when designing tables or reviewing Supabase Row Level Security. Prefer the user's migration framework or database client for project migrations. For old-skill compatibility, scripts/supabase_dataplane.py can execute SQL through the Supabase REST /pg/query endpoint.
Schema Defaults
- Table and column names:
snake_case - Primary key:
id bigserial primary keyorid uuid primary key default gen_random_uuid() - Timestamps: prefer
timestamptz - JSON: prefer
jsonb - Index names:
ix_<table>_<column> - Unique constraint names:
uq_<table>_<column>
Safe additive changes:
alter table public.posts add column if not exists tags text[];
alter table public.posts add column if not exists view_count integer not null default 0;
create index if not exists ix_posts_created_at on public.posts(created_at desc);Risky changes that need explicit review:
- Dropping columns or tables
- Changing column types
- Shortening string length
- Adding non-null columns without defaults
- Adding unique constraints when existing data may conflict
RLS Rules
Enable RLS for every table that is reachable through Supabase APIs:
alter table public.posts enable row level security;Common policy shapes:
create policy "posts_allow_public_read" on public.posts
for select using (true);
create policy "posts_auth_insert" on public.posts
for insert with check (auth.role() = 'authenticated');For user-owned rows:
alter table public.notes add column if not exists user_id uuid not null default auth.uid();
create policy "notes_owner_select" on public.notes
for select using (auth.uid() = user_id);
create policy "notes_owner_insert" on public.notes
for insert with check (auth.uid() = user_id);Check current RLS state:
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;Run a check through the preserved data-plane command when needed:
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select schemaname, tablename, rowsecurity from pg_tables where schemaname = 'public' order by tablename"SQL Playbook
Use this reference for common SQL inspection, CRUD, migration, pgvector, RLS, and RPC snippets. Execute through the preserved data-plane command when a direct SQL path is needed:
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select 1"For larger SQL files:
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query-file ./query.sqlInspect Schema
List tables:
python3 scripts/supabase_dataplane.py list-tables --workspace-id ws-xxxx --schemas public,authColumn details:
select column_name, data_type, is_nullable, column_default
from information_schema.columns
where table_schema = 'public' and table_name = 'posts'
order by ordinal_position;Indexes:
select indexname, indexdef
from pg_indexes
where schemaname = 'public' and tablename = 'posts';Foreign keys:
select conname, conrelid::regclass, confrelid::regclass, pg_get_constraintdef(oid)
from pg_constraint
where contype = 'f' and connamespace = 'public'::regnamespace;CRUD
select * from public.posts order by created_at desc limit 20;
insert into public.posts (title, content, published)
values ('hello', 'first post', false)
returning *;
update public.posts
set published = true, updated_at = now()
where id = 1
returning *;
delete from public.posts
where id = 1;Migrations
Apply reviewed SQL as a tracked migration:
python3 scripts/supabase_dataplane.py apply-migration --workspace-id ws-xxxx --name create_posts --query-file ./migrations/001_create_posts.sqlList migration records:
python3 scripts/supabase_dataplane.py list-migrations --workspace-id ws-xxxxThe compatibility migration wrapper writes to supabase_migrations.schema_migrations, matching the old skill behavior.
RLS Checks
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;Enable RLS and add a public-read policy:
alter table public.posts enable row level security;
create policy "posts_allow_public_read" on public.posts
for select using (true);User-owned row pattern:
alter table public.notes add column if not exists user_id uuid not null default auth.uid();
create policy "notes_owner_select" on public.notes
for select using (auth.uid() = user_id);
create policy "notes_owner_insert" on public.notes
for insert with check (auth.uid() = user_id);pgvector
create extension if not exists vector;
create table if not exists public.documents (
id bigserial primary key,
content text not null,
metadata jsonb,
embedding vector(1536),
created_at timestamptz not null default now()
);
create index if not exists ix_documents_embedding on public.documents
using hnsw (embedding vector_cosine_ops);Check installed extensions:
python3 scripts/supabase_dataplane.py list-extensions --workspace-id ws-xxxxTypeScript Types
Generate Supabase-style table types from information_schema.columns:
python3 scripts/supabase_dataplane.py generate-typescript-types --workspace-id ws-xxxx --schemas publicVolcengine Supabase Tool Reference
CLI Coverage
Current ve aidap coverage includes:
- Workspace:
DescribeWorkspaces,DescribeWorkspaceDetail,DescribeWorkspaceOverview,CreateWorkspace,DeleteWorkspace,StartWorkspace,StopWorkspace,ModifyWorkspaceSettings,ModifyWorkspaceName - Branch:
DescribeDefaultBranch,DescribeBranches,CreateBranch,DeleteBranch,ResetBranch,RestartBranch,SetAsDefaultBranch,UpdateBranch - Database and account:
DescribeDatabases,CreateDatabase,DropDatabase,DescribeDBAccounts,CreateDBAccount,DeleteDBAccount,ResetDBAccountPassword,DescribeDBAccountConnection - Endpoint and key:
DescribeWorkspaceEndpoint,DescribeAPIKeys,DescribeSupabaseDeployEnvVars,CreateEndpointPublicAddress,DeleteEndpointPublicAddress,CreateAccessControlList,ModifyAccessControlList - Schema diff:
CreateSchemaDiff,DescribeSchemaDiffJobStatus,DescribeSchemaDiffResultSQLText,DescribeSchemaDiffResultSQLTextAll
Run ve aidap <Action> --help before composing a command because parameter names and body shapes are authoritative there.
Branch-scoped operations require an explicit BranchId. This includes DescribeWorkspaceEndpoint, DescribeAPIKeys, DescribeSupabaseDeployEnvVars, DescribeDBAccounts, DescribeDatabases, CreateDBAccount, CreateDatabase, DescribeDBAccountConnection, and related endpoint/account/database actions. A real DescribeWorkspaceEndpoint call without BranchId returned InvalidParameter, so do not omit it even when the workspace has a default branch.
For PostgreSQL workspaces, endpoint and account-connection reads are also compute-scoped. Resolve the primary database compute first:
ve aidap DescribeWorkspaceDetail --WorkspaceId ws-xxxx
ve aidap DescribeDefaultBranch --WorkspaceId ws-xxxx
ve aidap DescribeComputes --WorkspaceId ws-xxxx --BranchId br-xxxxUse the ComputeId whose compute has ComputeName=Primary, ComputeRole=Primary, ServiceType=Database, and ComputeStatus=Active:
ve aidap DescribeWorkspaceEndpoint --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx
ve aidap DescribeDBAccountConnection --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx --DatabaseName postgres --AccountName user_adminA verified PostgreSQL DescribeWorkspaceEndpoint call with only BranchId returned InvalidParameter: 参数ComputeId值无效; do not retry unrelated branch IDs for that symptom.
PostgreSQL Workspace Bootstrap Notes
For database_engine=postgresql, prefer explicit networking in CreateWorkspace: pass NetworkSettings.VpcId and NetworkSettings.SubnetId, reusing an existing Available default VPC/subnet when appropriate. Do not begin with shared public/shared-network-only creation as the default path.
After creation, wait in this order:
1. DescribeWorkspaceDetail until WorkspaceStatus=Running. 2. DescribeDefaultBranch until BranchStatus=Ready. 3. DescribeComputes until the primary database compute is Active; save its ComputeId.
Only after those checks should you fetch endpoints or database-account connection strings.
Verified Control-Plane Failure Pattern
CreateDBAccount can return PrimaryComputeNotFound even when the default branch is ready. Before changing IDs, verify the same workspace and branch with:
ve aidap DescribeDefaultBranch --WorkspaceId ws-xxxx
ve aidap DescribeBranches --WorkspaceId ws-xxxx
ve aidap DescribeComputes --WorkspaceId ws-xxxx --BranchId br-xxxxIf DescribeComputes shows ComputeName=Primary, ComputeRole=Primary, ServiceType=Database, and ComputeStatus=Active for the target branch, record the case as a control-plane inconsistency. Use the POSTGRES_URL fallback described in deploy-provider.md instead of trying unrelated branch IDs.
Connection Secret Handling
Do not print complete PostgreSQL passwords or credential-bearing connection strings. Store DATABASE_URL in a local env file with directory mode 700 and file mode 600, then verify with a PostgreSQL client:
mkdir -p .aidap
chmod 700 .aidap
umask 077
printf 'DATABASE_URL=%q\n' "$DATABASE_URL" > .aidap/ws-xxxx-postgres.env
psql "$DATABASE_URL" -Atc "select 1"For final answers, report only the host/port, database/user names, resource IDs, local file path, and verification outcome.
ACL Verification
CreateAccessControlList exposes --IPList array, but common CLI array forms have returned InvalidParameterFormat in real PostgreSQL workspace testing. Do not assume the allowlist has been narrowed just because a create/modify command was attempted.
Always re-check the effective sources through DescribeDBAccountConnection:
ve aidap DescribeDBAccountConnection --WorkspaceId ws-xxxx --BranchId br-xxxx --ComputeId cp-xxxx --DatabaseName postgres --AccountName user_adminInspect Result.AllowHost. If it includes 0.0.0.0/0 or ::/0, warn that public access is broad and recommend tightening it in the console or with a separately verified API invocation.
Engine Model
AIDAP deploy-facing database engine choices are stable product-level choices:
database_engine=supabasedatabase_engine=postgresql
Current official CreateWorkspace API enum mapping from the Volcengine docs:
| Deploy choice | EngineType | EngineVersion | API description |
|---|---|---|---|
database_engine=postgresql | PostgreSQL | PostgreSQL_17 | PostgreSQL 17 |
database_engine=supabase | Supabase | Supabase_1_24 | Supabase 1.24 |
| Not a deploy default | veDB_MySQL | veDB_MySQL_8_0 | veDB MySQL 8.0 |
Source: official CreateWorkspace docs, https://www.volcengine.com/docs/87275/2105881?lang=zh.
EngineVersion is required. EngineType supports PostgreSQL, veDB_MySQL, and Supabase; include it in generated bodies to avoid ambiguity even though the API marks it optional. For EngineVersion=veDB_MySQL_8_0 or EngineVersion=Supabase_1_24, the official docs require NetworkSettings.
Before a live workspace creation, re-check the current CreateWorkspace documentation, console payload, or CLI/API evidence if this table may be stale. Keep enum values centralized here; do not repeat version labels throughout prepare/deploy selection docs.
Data-Plane Coverage
Current ve aidap does not expose these old-skill capabilities for Supabase-compatible workspaces. Use scripts/supabase_dataplane.py:
- Database:
execute-sql,list-tables,list-migrations,list-extensions,apply-migration,generate-typescript-types - Edge Functions:
list-edge-functions,get-edge-function,deploy-edge-function,delete-edge-function - Storage:
list-storage-buckets,create-storage-bucket,delete-storage-bucket,get-storage-config
The script keeps the old action names. It resolves workspace endpoint, default branch, and service-role key through ve aidap; the actual data-plane request is a Supabase REST request with:
apikey: <service-role-key>
Authorization: Bearer <service-role-key>It does not sign data-plane requests with Volcengine AK/SK.
Examples:
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query "select 1"
python3 scripts/supabase_dataplane.py execute-sql --workspace-id ws-xxxx --query-file ./query.sql
python3 scripts/supabase_dataplane.py apply-migration --workspace-id ws-xxxx --name add_table --query-file ./migration.sql
python3 scripts/supabase_dataplane.py generate-typescript-types --workspace-id ws-xxxx --schemas public
python3 scripts/supabase_dataplane.py list-edge-functions --workspace-id ws-xxxx
python3 scripts/supabase_dataplane.py deploy-edge-function --workspace-id ws-xxxx --function-name hello --source-file ./index.ts
python3 scripts/supabase_dataplane.py list-storage-buckets --workspace-id ws-xxxx
python3 scripts/supabase_dataplane.py create-storage-bucket --workspace-id ws-xxxx --bucket-name uploads --publicEnvironment:
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYcan be set to skip endpoint/key lookup.DEFAULT_WORKSPACE_IDis used when--workspace-idis omitted.SUPABASE_WORKSPACE_SLUGdefaults todefaultfor Edge Function routes.SUPABASE_ENDPOINT_SCHEMEdefaults tohttpto match the old skill's endpoint URL behavior.READ_ONLY=trueblocks data-plane write actions.
Bootstrap API Missing From ve
Use scripts/aidap_bootstrap.py only for enterprise verification checks:
python3 scripts/aidap_bootstrap.py get-verify-infoget-verify-info calls account_verify/GetVerifyInfo/2018-01-01 without request parameters. Treat the account as enterprise verified only when the response has IsVerified=true and IdentityType="enterprise"; the script emits verification.enterprise_verified from that rule.
The bootstrap script requires VOLCENGINE_ACCESS_KEY and VOLCENGINE_SECRET_KEY in the environment. It does not read or reuse cached ve login credentials.
If either non-public API changes, adjust with:
python3 scripts/aidap_bootstrap.py <operation> --method <GET|POST> --params '{"Key":"Value"}'Service Activation
When the account has not enabled AIDAP, ask the user to open:
https://console.volcengine.com/iam/service/attach_role/?ServiceName=aidapAfter the console flow completes, rerun a read action such as:
ve aidap DescribeWorkspaces --Limit 1Safety
- Treat
Create*,Modify*,Reset*,Start*,Stop*, andDelete*as write operations. Show the exact command and wait for user confirmation unless the user already explicitly asked to perform that change. - Treat
DeleteWorkspace,DeleteBranch,DropDatabase, and password/key reset actions as destructive. - Treat
apply-migration, Edge Function deploy/delete, and Storage bucket create/delete as write or destructive data-plane operations. - Do not expose complete API keys, service-role keys, account passwords, or credential-bearing database URLs in final output.
#!/usr/bin/env python3
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: MIT
"""Call AIDAP bootstrap APIs not exposed by the current ve CLI."""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import hmac
import json
import os
import sys
from typing import Any
from urllib import error as urllib_error
from urllib import request as urllib_request
from urllib.parse import quote, urlencode
DEFAULT_REGION = "cn-beijing"
DEFAULT_HOST = "open.volcengineapi.com"
API_REGISTRY = {
"get-verify-info": {
"service": "account_verify",
"action": "GetVerifyInfo",
"version": "2018-01-01",
"method": "POST",
"body": {},
"summary": "Check whether the account has enterprise real-name verification.",
},
}
def env(name: str, default: str | None = None) -> str | None:
value = os.getenv(name)
return value if value not in (None, "") else default
def norm_query(params: dict[str, Any]) -> str:
pairs: list[str] = []
for key in sorted(params.keys()):
value = params[key]
if isinstance(value, list):
for item in value:
pairs.append(f"{quote(key, safe='-_.~')}={quote(str(item), safe='-_.~')}")
else:
pairs.append(f"{quote(key, safe='-_.~')}={quote(str(value), safe='-_.~')}")
return "&".join(pairs).replace("+", "%20")
def hmac_sha256(key: bytes, content: str) -> bytes:
return hmac.new(key, content.encode("utf-8"), hashlib.sha256).digest()
def hash_sha256(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
def parse_json(value: str | None) -> dict[str, Any]:
if not value:
return {}
try:
payload = json.loads(value)
except json.JSONDecodeError as exc:
raise SystemExit(f"invalid JSON for --params: {exc}") from exc
if not isinstance(payload, dict):
raise SystemExit("--params must be a JSON object")
return payload
def summarize_verify_info(payload: Any) -> dict[str, Any]:
info = payload.get("Result", payload) if isinstance(payload, dict) else {}
if not isinstance(info, dict):
info = {}
is_verified = info.get("IsVerified") is True
identity_type = info.get("IdentityType")
return {
"is_verified": is_verified,
"identity_type": identity_type,
"enterprise_verified": is_verified and identity_type == "enterprise",
}
def signed_action_request(
*,
ak: str,
sk: str,
session_token: str,
region: str,
host: str,
service: str,
version: str,
action: str,
method: str,
body: dict[str, Any],
scheme: str,
content_type: str = "application/json",
) -> tuple[Any, int]:
method = method.upper()
if method == "GET":
request_query = {"Action": action, "Version": version, **body}
body_str = ""
else:
request_query = {"Action": action, "Version": version}
body_str = urlencode(body, doseq=True) if content_type == "application/x-www-form-urlencoded" else json.dumps(body)
x_date = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
short_date = x_date[:8]
body_hash = hash_sha256(body_str)
signed_headers = "content-type;host;x-content-sha256;x-date"
canonical_request = "\n".join(
[
method,
"/",
norm_query(request_query),
"\n".join(
[
f"content-type:{content_type}",
f"host:{host}",
f"x-content-sha256:{body_hash}",
f"x-date:{x_date}",
]
),
"",
signed_headers,
body_hash,
]
)
credential_scope = "/".join([short_date, region, service, "request"])
string_to_sign = "\n".join(["HMAC-SHA256", x_date, credential_scope, hash_sha256(canonical_request)])
k_date = hmac_sha256(sk.encode("utf-8"), short_date)
k_region = hmac_sha256(k_date, region)
k_service = hmac_sha256(k_region, service)
k_signing = hmac_sha256(k_service, "request")
signature = hmac_sha256(k_signing, string_to_sign).hex()
headers = {
"Host": host,
"X-Content-Sha256": body_hash,
"X-Date": x_date,
"Content-Type": content_type,
"Authorization": (
f"HMAC-SHA256 Credential={ak}/{credential_scope}, "
f"SignedHeaders={signed_headers}, Signature={signature}"
),
}
if session_token:
headers["X-Security-Token"] = session_token
url = f"{scheme}://{host}/?{norm_query(request_query)}"
data = None if method == "GET" else body_str.encode("utf-8")
req = urllib_request.Request(url=url, data=data, headers=headers, method=method)
try:
with urllib_request.urlopen(req, timeout=30) as response:
status_code = response.status
response_text = response.read().decode("utf-8")
except urllib_error.HTTPError as exc:
status_code = exc.code
response_text = exc.read().decode("utf-8")
try:
payload: Any = json.loads(response_text)
except json.JSONDecodeError:
payload = response_text
return payload, status_code
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("operation", choices=sorted(API_REGISTRY))
parser.add_argument("--params", help="JSON object merged into the default request body")
parser.add_argument("--region", default=env("VOLCENGINE_REGION", DEFAULT_REGION))
parser.add_argument("--host", default=env("VOLCENGINE_ENDPOINT", DEFAULT_HOST))
parser.add_argument("--scheme", default="https", choices=["http", "https"])
parser.add_argument("--method", choices=["GET", "POST"])
parser.add_argument("--content-type", default="application/json")
parser.add_argument("--output", choices=["json", "pretty"], default="json")
return parser
def main() -> int:
args = build_parser().parse_args()
ak = env("VOLCENGINE_ACCESS_KEY")
sk = env("VOLCENGINE_SECRET_KEY")
if not ak or not sk:
print("VOLCENGINE_ACCESS_KEY and VOLCENGINE_SECRET_KEY are required", file=sys.stderr)
return 2
spec = API_REGISTRY[args.operation]
body = dict(spec["body"])
body.update(parse_json(args.params))
payload, status_code = signed_action_request(
ak=ak,
sk=sk,
session_token=env("VOLCENGINE_SESSION_TOKEN", "") or "",
region=args.region,
host=args.host,
service=spec["service"],
version=spec["version"],
action=spec["action"],
method=args.method or spec["method"],
body=body,
scheme=args.scheme,
content_type=args.content_type,
)
result = {
"operation": args.operation,
"service": spec["service"],
"action": spec["action"],
"version": spec["version"],
"status_code": status_code,
"response": payload,
}
if args.operation == "get-verify-info":
result["verification"] = summarize_verify_info(payload)
if args.output == "pretty":
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(json.dumps(result, ensure_ascii=False))
return 0 if 200 <= status_code < 300 else 1
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: MIT
"""Supabase data-plane operations not exposed by `ve aidap`."""
from __future__ import annotations
import argparse
import datetime as dt
import json
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
from urllib import error as urllib_error
from urllib import request as urllib_request
from urllib.parse import quote, urlencode
DATABASE_ACTIONS = {
"execute-sql",
"list-tables",
"list-migrations",
"list-extensions",
"apply-migration",
"generate-typescript-types",
}
EDGE_ACTIONS = {
"list-edge-functions",
"get-edge-function",
"deploy-edge-function",
"delete-edge-function",
}
STORAGE_ACTIONS = {
"list-storage-buckets",
"create-storage-bucket",
"delete-storage-bucket",
"get-storage-config",
}
WRITE_ACTIONS = {
"apply-migration",
"deploy-edge-function",
"delete-edge-function",
"create-storage-bucket",
"delete-storage-bucket",
}
RUNTIME_CONFIG = {
"native-node20/v1": {"entrypoint": "index.ts", "extensions": [".ts", ".js"]},
"native-python3.9/v1": {"entrypoint": "app.py", "extensions": [".py"]},
"native-python3.10/v1": {"entrypoint": "app.py", "extensions": [".py"]},
"native-python3.12/v1": {"entrypoint": "app.py", "extensions": [".py"]},
}
MAX_CODE_SIZE = 10 * 1024 * 1024
class DataPlaneError(RuntimeError):
pass
class SupabaseApiError(DataPlaneError):
def __init__(self, status_code: int, path: str, endpoint: str, payload: Any):
self.status_code = status_code
self.path = path
self.endpoint = endpoint
self.payload = payload
super().__init__(
json.dumps(
{
"status_code": status_code,
"path": path,
"endpoint": endpoint,
"error": payload,
},
ensure_ascii=False,
)
)
def env(name: str, default: str | None = None) -> str | None:
value = os.getenv(name)
return value if value not in (None, "") else default
def read_text(value: str | None, file_path: str | None, label: str) -> str:
if value and file_path:
raise DataPlaneError(f"{label} and {label}-file cannot be used together")
if file_path:
return Path(file_path).read_text(encoding="utf-8")
if value:
return value
raise DataPlaneError(f"{label} or {label}-file is required")
def to_json(payload: Any) -> str:
return json.dumps(payload, indent=2, ensure_ascii=False)
def parse_json_object(value: str | None, label: str) -> dict[str, Any]:
if not value:
return {}
try:
payload = json.loads(value)
except json.JSONDecodeError as exc:
raise DataPlaneError(f"invalid JSON for {label}: {exc}") from exc
if not isinstance(payload, dict):
raise DataPlaneError(f"{label} must be a JSON object")
return payload
def find_json_payload(text: str) -> Any:
decoder = json.JSONDecoder()
for index, char in enumerate(text):
if char not in "[{":
continue
try:
payload, _ = decoder.raw_decode(text[index:])
return payload
except json.JSONDecodeError:
continue
raise DataPlaneError("could not parse JSON from ve output")
def run_ve_aidap(action: str, body: dict[str, Any], region: str | None = None) -> Any:
cmd = ["ve", "aidap", action]
if body:
cmd.extend(["--body", json.dumps(body, separators=(",", ":"), ensure_ascii=False)])
if region:
cmd.extend(["---region", region])
completed = subprocess.run(cmd, check=False, text=True, capture_output=True)
output = "\n".join(part for part in (completed.stdout, completed.stderr) if part)
if completed.returncode != 0:
raise DataPlaneError(f"ve aidap {action} failed: {output.strip()}")
payload = find_json_payload(output)
if isinstance(payload, dict) and "Result" in payload:
return payload["Result"]
return payload
def pick(source: Any, *field_names: str) -> Any:
if not isinstance(source, dict):
return None
lowered = {str(key).lower(): value for key, value in source.items()}
for field_name in field_names:
value = source.get(field_name)
if value is None:
value = lowered.get(field_name.lower())
if isinstance(value, str):
value = value.strip() or None
if value is not None:
return value
return None
def walk(value: Any) -> list[Any]:
result = [value]
if isinstance(value, dict):
for item in value.values():
result.extend(walk(item))
elif isinstance(value, list):
for item in value:
result.extend(walk(item))
return result
def dicts(value: Any) -> list[dict[str, Any]]:
return [item for item in walk(value) if isinstance(item, dict)]
def strings(value: Any) -> list[str]:
return [item.strip() for item in walk(value) if isinstance(item, str) and item.strip()]
def looks_like_branch_id(value: str | None) -> bool:
return bool(value and value.strip().startswith("br-"))
def normalize_endpoint(value: str, scheme: str) -> str | None:
text = value.strip()
if not text:
return None
if text.startswith(("http://", "https://")):
return text.rstrip("/")
if " " in text or "/" in text or "." not in text:
return None
if scheme == "http":
return f"http://{text}:80"
return f"https://{text}"
def extract_endpoint(payload: Any, scheme: str) -> str:
scheme = (scheme or "http").strip().lower() or "http"
preferred: list[str] = []
fallback: list[str] = []
for item in strings(payload):
endpoint = normalize_endpoint(item, scheme)
if not endpoint:
continue
if "volces.com" in endpoint and "ivolces.com" not in endpoint:
preferred.append(endpoint)
else:
fallback.append(endpoint)
if preferred:
return preferred[0]
if fallback:
return fallback[0]
raise DataPlaneError("could not find Supabase endpoint in ve response")
def extract_branch_id(payload: Any) -> str | None:
for item in dicts(payload):
branch_id = pick(item, "BranchId", "branch_id", "Id", "id")
if isinstance(branch_id, str) and looks_like_branch_id(branch_id):
return branch_id
for item in strings(payload):
if looks_like_branch_id(item):
return item
return None
def extract_workspace_ids(payload: Any) -> list[str]:
workspace_ids: list[str] = []
for item in dicts(payload):
workspace_id = pick(item, "WorkspaceId", "workspace_id", "Id", "id")
if isinstance(workspace_id, str) and workspace_id and workspace_id not in workspace_ids:
workspace_ids.append(workspace_id)
return workspace_ids
def extract_api_key(payload: Any, preferred_type: str = "service") -> str:
candidates: list[tuple[int, str]] = []
for item in dicts(payload):
key_value = pick(item, "Key", "key", "ApiKey", "api_key", "APIKey", "Value", "value")
if not isinstance(key_value, str) or len(key_value) < 16:
continue
key_type = str(pick(item, "Type", "type", "KeyType", "key_type", "Name", "name") or "").lower()
score = 0
if preferred_type.lower() in key_type or "service_role" in key_type:
score = 10
elif "service" in key_type:
score = 8
elif "public" in key_type or "anon" in key_type:
score = 1
candidates.append((score, key_value))
if not candidates:
raise DataPlaneError("could not find API key in ve response")
candidates.sort(key=lambda item: item[0], reverse=True)
return candidates[0][1]
def resolve_workspace_for_branch(branch_id: str, region: str | None = None) -> str:
workspaces = run_ve_aidap("DescribeWorkspaces", {"Limit": 100}, region)
for workspace_id in extract_workspace_ids(workspaces):
branches = run_ve_aidap("DescribeBranches", {"WorkspaceId": workspace_id}, region)
for item in dicts(branches):
if pick(item, "BranchId", "branch_id", "Id", "id") == branch_id:
return workspace_id
raise DataPlaneError(f"could not resolve workspace for branch {branch_id}")
def resolve_workspace_and_branch(args: argparse.Namespace) -> tuple[str | None, str | None]:
workspace_id = args.workspace_id or args.default_workspace_id or env("DEFAULT_WORKSPACE_ID")
branch_id = args.branch_id
if workspace_id and looks_like_branch_id(workspace_id):
branch_id = workspace_id
workspace_id = resolve_workspace_for_branch(branch_id, args.region)
if workspace_id and not branch_id:
payload = run_ve_aidap("DescribeDefaultBranch", {"WorkspaceId": workspace_id}, args.region)
branch_id = extract_branch_id(payload)
return workspace_id, branch_id
def resolve_endpoint_and_key(args: argparse.Namespace) -> tuple[str, str, str | None, str | None]:
endpoint = env("SUPABASE_URL")
key = env("SUPABASE_SERVICE_ROLE_KEY")
workspace_id: str | None = None
branch_id: str | None = args.branch_id
if not endpoint or not key:
workspace_id, branch_id = resolve_workspace_and_branch(args)
if not workspace_id:
raise DataPlaneError(
"workspace_id is required unless SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are set"
)
if not endpoint:
endpoint_payload = run_ve_aidap(
"DescribeWorkspaceEndpoint",
{"WorkspaceId": workspace_id, "BranchId": branch_id},
args.region,
)
endpoint = extract_endpoint(endpoint_payload, args.endpoint_scheme)
if not key:
key_payload = run_ve_aidap(
"DescribeAPIKeys",
{"WorkspaceId": workspace_id, "BranchId": branch_id, "Limit": 100},
args.region,
)
key = extract_api_key(key_payload, "service")
return endpoint.rstrip("/"), key, workspace_id, branch_id
def call_supabase_api(
endpoint: str,
api_key: str,
path: str,
method: str = "GET",
json_data: Any | None = None,
params: dict[str, Any] | None = None,
content: bytes | None = None,
timeout: float = 30.0,
) -> Any:
url = f"{endpoint}{path}"
if params:
url = f"{url}?{urlencode(params, doseq=True)}"
headers = {
"apikey": api_key,
"Authorization": f"Bearer {api_key}",
}
data = content
if json_data is not None:
data = json.dumps(json_data, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
for attempt in range(3):
req = urllib_request.Request(url=url, data=data, headers=headers, method=method)
try:
with urllib_request.urlopen(req, timeout=timeout) as response:
raw = response.read()
if response.status == 204 or not raw:
return {"success": True}
text = raw.decode("utf-8")
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
return json.loads(text)
try:
return json.loads(text)
except json.JSONDecodeError:
return {"raw": text}
except urllib_error.HTTPError as exc:
payload: Any
text = exc.read().decode("utf-8")
try:
payload = json.loads(text)
except json.JSONDecodeError:
payload = text
if exc.code in {502, 503, 504} and attempt < 2:
time.sleep(0.5 * (attempt + 1))
continue
raise SupabaseApiError(exc.code, path, endpoint, payload) from exc
except urllib_error.URLError as exc:
if attempt < 2:
time.sleep(0.5 * (attempt + 1))
continue
raise DataPlaneError(f"{exc.reason} [endpoint: {endpoint}, path: {path}]") from exc
raise DataPlaneError(f"request failed [endpoint: {endpoint}, path: {path}]")
class DataPlaneClient:
def __init__(self, endpoint: str, api_key: str, workspace_slug: str = "default"):
self.endpoint = endpoint.rstrip("/")
self.api_key = api_key
self.workspace_slug = workspace_slug.strip() or "default"
def call_api(self, path: str, method: str = "GET", json_data: Any | None = None, params: dict[str, Any] | None = None) -> Any:
return call_supabase_api(self.endpoint, self.api_key, path, method=method, json_data=json_data, params=params)
def execute_sql_raw(self, query: str) -> list[dict[str, Any]]:
if not query or not query.strip():
raise DataPlaneError("SQL query cannot be empty")
result = self.call_api("/pg/query", method="POST", json_data={"query": query})
if isinstance(result, dict) and isinstance(result.get("data"), list):
result = result["data"]
if not isinstance(result, list):
raise DataPlaneError(f"unexpected SQL result type: {type(result).__name__}")
return result
def list_edge_functions(self) -> Any:
return self.call_api(f"/v1/projects/{quote(self.workspace_slug, safe='')}/functions")
def get_edge_function(self, function_name: str) -> Any:
encoded_name = quote(function_name, safe="")
result = self.call_api(f"/v1/projects/{quote(self.workspace_slug, safe='')}/functions/{encoded_name}")
return normalize_function_payload(result)
def deploy_edge_function(
self,
function_name: str,
source_code: str,
verify_jwt: bool,
runtime: str,
import_map: str | None = None,
) -> Any:
data = build_deployment_payload(runtime, source_code, verify_jwt, function_name)
if import_map:
try:
import_map_data = json.loads(import_map)
except json.JSONDecodeError as exc:
raise DataPlaneError(f"invalid import map JSON: {exc}") from exc
data["metadata"]["import_map_path"] = "import_map.json"
data["files"].append({"name": "import_map.json", "content": json.dumps(import_map_data)})
encoded_name = quote(function_name, safe="")
result = self.call_api(
f"/v1/projects/{quote(self.workspace_slug, safe='')}/functions/deploy",
method="POST",
params={"slug": encoded_name},
json_data=data,
)
if isinstance(result, dict) and not result.get("runtime"):
result["runtime"] = runtime
return result
def delete_edge_function(self, function_name: str) -> dict[str, Any]:
encoded_name = quote(function_name, safe="")
self.call_api(f"/v1/projects/{quote(self.workspace_slug, safe='')}/functions/{encoded_name}", method="DELETE")
return {"success": True, "message": "Edge function deleted successfully"}
def list_storage_buckets(self) -> Any:
return self.call_api("/storage/v1/bucket")
def create_storage_bucket(
self,
bucket_name: str,
public: bool,
file_size_limit: int | None,
allowed_mime_types: str | list[str] | None,
) -> Any:
if not bucket_name or not bucket_name.strip():
raise DataPlaneError("Bucket name cannot be empty")
data: dict[str, Any] = {"name": bucket_name, "public": public}
if file_size_limit:
data["file_size_limit"] = file_size_limit
normalized_mime_types = normalize_allowed_mime_types(allowed_mime_types)
if normalized_mime_types:
data["allowed_mime_types"] = normalized_mime_types
return self.call_api("/storage/v1/bucket", method="POST", json_data=data)
def delete_storage_bucket(self, bucket_name: str) -> dict[str, Any]:
if not bucket_name or not bucket_name.strip():
raise DataPlaneError("Bucket name cannot be empty")
encoded_bucket = quote(bucket_name, safe="")
response = self.call_api(f"/storage/v1/bucket/{encoded_bucket}", method="DELETE")
if isinstance(response, dict) and response.get("error"):
raise DataPlaneError(str(response["error"]))
return {"success": True, "message": "Bucket deleted successfully"}
def get_storage_config(self) -> Any:
return self.call_api("/storage/v1/config")
def needs_handler_wrapper(runtime: str, source_code: str) -> bool:
if runtime != "native-node20/v1":
return False
if "Deno.serve" in source_code:
return False
return "export default function" in source_code or "export default async function" in source_code or "export default (" in source_code
def validate_runtime(runtime: str) -> None:
if runtime not in RUNTIME_CONFIG:
available = ", ".join(RUNTIME_CONFIG)
raise DataPlaneError(f"unsupported runtime '{runtime}'. Available: {available}")
def build_deployment_payload(runtime: str, source_code: str, verify_jwt: bool, function_name: str) -> dict[str, Any]:
validate_runtime(runtime)
if not source_code or not source_code.strip():
raise DataPlaneError("Source code cannot be empty")
source_code = source_code.replace("<", "<").replace(">", ">").replace("&", "&")
code_size = len(source_code.encode("utf-8"))
if code_size > MAX_CODE_SIZE:
raise DataPlaneError(f"Source code too large: {code_size} bytes (max {MAX_CODE_SIZE} bytes)")
entrypoint = RUNTIME_CONFIG[runtime]["entrypoint"]
files = [{"name": entrypoint, "content": source_code}]
if needs_handler_wrapper(runtime, source_code):
files = [
{"name": "handler.ts", "content": source_code},
{"name": entrypoint, "content": "import handler from './handler.ts'\nDeno.serve((req) => handler(req))\n"},
]
return {
"metadata": {
"name": function_name,
"slug": function_name,
"entrypoint_path": entrypoint,
"verify_jwt": verify_jwt,
},
"files": files,
}
def normalize_function_payload(payload: Any) -> Any:
if not isinstance(payload, dict):
return payload
result = dict(payload)
files = result.get("files")
entrypoint_path = result.get("entrypoint_path")
if isinstance(files, list):
source_code = None
for file_info in files:
if not isinstance(file_info, dict):
continue
if entrypoint_path and file_info.get("name") == entrypoint_path and isinstance(file_info.get("content"), str):
source_code = file_info.get("content")
break
if source_code is None and isinstance(file_info.get("content"), str):
source_code = file_info.get("content")
if source_code is not None:
result["source_code"] = source_code
return result
def normalize_allowed_mime_types(allowed_mime_types: str | list[str] | None) -> list[str] | None:
if allowed_mime_types is None:
return None
if isinstance(allowed_mime_types, list):
values = allowed_mime_types
elif isinstance(allowed_mime_types, str):
text = allowed_mime_types.strip()
if not text:
return None
if text.startswith("["):
parsed = json.loads(text)
if not isinstance(parsed, list):
raise DataPlaneError("allowed_mime_types JSON value must be a list of strings")
values = parsed
else:
values = text.split(",")
else:
raise DataPlaneError("allowed_mime_types must be a string, JSON array string, or list of strings")
result = [value.strip() for value in values if isinstance(value, str) and value.strip()]
return result or None
def to_ts_type(data_type: str, udt_name: str) -> str:
normalized_data_type = (data_type or "").lower()
normalized_udt_name = (udt_name or "").lower()
if normalized_data_type in {"smallint", "integer", "bigint", "numeric", "decimal", "real", "double precision"}:
return "number"
if normalized_data_type == "boolean":
return "boolean"
if normalized_data_type in {"json", "jsonb"}:
return "Json"
if normalized_data_type in {"date", "timestamp without time zone", "timestamp with time zone", "time without time zone", "time with time zone"}:
return "string"
if normalized_data_type == "bytea":
return "string"
if normalized_data_type == "array":
base = normalized_udt_name[1:] if normalized_udt_name.startswith("_") else normalized_udt_name
return f"{to_ts_type(base, base)}[]"
if normalized_udt_name in {"uuid", "varchar", "text", "bpchar", "name", "citext", "inet"}:
return "string"
if normalized_udt_name in {"int2", "int4", "int8", "float4", "float8"}:
return "number"
if normalized_udt_name == "bool":
return "boolean"
if normalized_udt_name in {"json", "jsonb"}:
return "Json"
return "string"
def to_ts_key(key: str) -> str:
if key and key.replace("_", "").isalnum() and not key[0].isdigit():
return key
escaped = key.replace("\\", "\\\\").replace("'", "\\'")
return f"'{escaped}'"
def build_typescript_types(columns: list[dict[str, Any]]) -> str:
grouped: dict[str, dict[str, list[dict[str, Any]]]] = {}
for column in columns:
schema_name = column.get("table_schema")
table_name = column.get("table_name")
if not schema_name or not table_name:
continue
grouped.setdefault(str(schema_name), {}).setdefault(str(table_name), []).append(column)
lines = [
"export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]",
"",
"export type Database = {",
]
for schema_name in sorted(grouped):
tables = grouped[schema_name]
lines.append(f" {to_ts_key(schema_name)}: {{")
lines.append(" Tables: {")
for table_name in sorted(tables):
table_columns = tables[table_name]
lines.append(f" {to_ts_key(table_name)}: {{")
lines.append(" Row: {")
for column in table_columns:
col_name = str(column.get("column_name"))
base_type = to_ts_type(str(column.get("data_type", "")), str(column.get("udt_name", "")))
nullable = column.get("is_nullable") == "YES"
row_type = f"{base_type} | null" if nullable else base_type
lines.append(f" {to_ts_key(col_name)}: {row_type}")
lines.append(" }")
lines.append(" Insert: {")
for column in table_columns:
col_name = str(column.get("column_name"))
base_type = to_ts_type(str(column.get("data_type", "")), str(column.get("udt_name", "")))
nullable = column.get("is_nullable") == "YES"
has_default = column.get("column_default") is not None
is_identity = column.get("is_identity") == "YES"
optional = nullable or has_default or is_identity
insert_type = f"{base_type} | null" if nullable else base_type
suffix = "?" if optional else ""
lines.append(f" {to_ts_key(col_name)}{suffix}: {insert_type}")
lines.append(" }")
lines.append(" Update: {")
for column in table_columns:
col_name = str(column.get("column_name"))
base_type = to_ts_type(str(column.get("data_type", "")), str(column.get("udt_name", "")))
nullable = column.get("is_nullable") == "YES"
update_type = f"{base_type} | null" if nullable else base_type
lines.append(f" {to_ts_key(col_name)}?: {update_type}")
lines.append(" }")
lines.append(" }")
lines.append(" }")
lines.append(" Views: {}")
lines.append(" Functions: {}")
lines.append(" Enums: {}")
lines.append(" CompositeTypes: {}")
lines.append(" }")
lines.append("}")
return "\n".join(lines)
def validate_schemas(schemas: list[str]) -> None:
for schema in schemas:
if not schema.replace("_", "").isalnum():
raise DataPlaneError(f"Invalid schema name: {schema}")
def execute_action(client: DataPlaneClient, args: argparse.Namespace) -> Any:
action = args.action
if action == "execute-sql":
query = read_text(args.query, args.query_file, "--query")
return client.execute_sql_raw(query)
if action == "list-tables":
schemas = [schema.strip() for schema in args.schemas.split(",") if schema.strip()]
validate_schemas(schemas)
schema_list = "', '".join(schemas)
query = f"""
SELECT schemaname as schema, tablename as name
FROM pg_tables
WHERE schemaname IN ('{schema_list}')
ORDER BY schemaname, tablename
"""
return client.execute_sql_raw(query)
if action == "list-migrations":
query = """
CREATE SCHEMA IF NOT EXISTS supabase_migrations;
CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (
version text PRIMARY KEY,
name text NOT NULL,
inserted_at timestamptz NOT NULL DEFAULT now()
);
SELECT version, name
FROM supabase_migrations.schema_migrations
ORDER BY version DESC
"""
return client.execute_sql_raw(query)
if action == "list-extensions":
query = """
SELECT e.extname AS name, n.nspname AS schema, e.extversion AS version
FROM pg_extension e
JOIN pg_namespace n ON n.oid = e.extnamespace
ORDER BY e.extname
"""
return client.execute_sql_raw(query)
if action == "apply-migration":
name = (args.name or "").strip()
if not name:
raise DataPlaneError("--name is required")
query = read_text(args.query, args.query_file, "--query")
migration_name = name.replace("'", "''")
migration_version = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%d%H%M%S%f")
migration_sql = f"""
BEGIN;
CREATE SCHEMA IF NOT EXISTS supabase_migrations;
CREATE TABLE IF NOT EXISTS supabase_migrations.schema_migrations (
version text PRIMARY KEY,
name text NOT NULL,
inserted_at timestamptz NOT NULL DEFAULT now()
);
{query}
INSERT INTO supabase_migrations.schema_migrations (version, name)
VALUES ('{migration_version}', '{migration_name}')
ON CONFLICT (version) DO UPDATE SET name = EXCLUDED.name;
COMMIT;
"""
client.execute_sql_raw(migration_sql)
return {
"success": True,
"message": f"Migration {name} applied successfully",
"version": migration_version,
"name": name,
}
if action == "generate-typescript-types":
schemas = [schema.strip() for schema in args.schemas.split(",") if schema.strip()]
validate_schemas(schemas)
schema_list = "', '".join(schemas)
query = f"""
SELECT table_schema, table_name, column_name, is_nullable, is_identity, data_type, udt_name, column_default
FROM information_schema.columns
WHERE table_schema IN ('{schema_list}')
ORDER BY table_schema, table_name, ordinal_position
"""
return build_typescript_types(client.execute_sql_raw(query))
if action == "list-edge-functions":
return client.list_edge_functions()
if action == "get-edge-function":
if not args.function_name:
raise DataPlaneError("--function-name is required")
return client.get_edge_function(args.function_name)
if action == "deploy-edge-function":
if not args.function_name:
raise DataPlaneError("--function-name is required")
source_code = read_text(args.source_code, args.source_file, "--source-code")
import_map = read_text(args.import_map, args.import_map_file, "--import-map") if args.import_map or args.import_map_file else None
return client.deploy_edge_function(args.function_name, source_code, args.verify_jwt, args.runtime, import_map)
if action == "delete-edge-function":
if not args.function_name:
raise DataPlaneError("--function-name is required")
return client.delete_edge_function(args.function_name)
if action == "list-storage-buckets":
return client.list_storage_buckets()
if action == "create-storage-bucket":
if not args.bucket_name:
raise DataPlaneError("--bucket-name is required")
return client.create_storage_bucket(args.bucket_name, args.public, args.file_size_limit, args.allowed_mime_types)
if action == "delete-storage-bucket":
if not args.bucket_name:
raise DataPlaneError("--bucket-name is required")
return client.delete_storage_bucket(args.bucket_name)
if action == "get-storage-config":
return client.get_storage_config()
supported = sorted(DATABASE_ACTIONS | EDGE_ACTIONS | STORAGE_ACTIONS)
raise DataPlaneError(f"Unsupported action: {action}. Available actions: {', '.join(supported)}")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("action", choices=sorted(DATABASE_ACTIONS | EDGE_ACTIONS | STORAGE_ACTIONS))
parser.add_argument("--workspace-id", help="Workspace ID, or branch ID for compatibility with the old skill")
parser.add_argument("--branch-id", help="Branch ID. Defaults to the workspace default branch")
parser.add_argument("--default-workspace-id", default=env("DEFAULT_WORKSPACE_ID"))
parser.add_argument("--region", help="Region passed to ve as ---region")
parser.add_argument("--endpoint-scheme", default=env("SUPABASE_ENDPOINT_SCHEME", "http"))
parser.add_argument("--workspace-slug", default=env("SUPABASE_WORKSPACE_SLUG", "default"))
parser.add_argument("--query")
parser.add_argument("--query-file")
parser.add_argument("--schemas", default="public")
parser.add_argument("--name")
parser.add_argument("--function-name")
parser.add_argument("--source-code")
parser.add_argument("--source-file")
parser.add_argument("--verify-jwt", dest="verify_jwt", action="store_true", default=True)
parser.add_argument("--no-verify-jwt", dest="verify_jwt", action="store_false")
parser.add_argument("--runtime", default="native-node20/v1")
parser.add_argument("--import-map")
parser.add_argument("--import-map-file")
parser.add_argument("--bucket-name")
parser.add_argument("--public", action="store_true")
parser.add_argument("--file-size-limit", type=int)
parser.add_argument("--allowed-mime-types")
return parser
def main() -> int:
args = build_parser().parse_args()
if args.action in WRITE_ACTIONS and str(env("READ_ONLY", "false")).lower() == "true":
print(to_json({"error": f"Cannot execute {args.action} in read-only mode"}))
return 1
try:
endpoint, api_key, _, _ = resolve_endpoint_and_key(args)
client = DataPlaneClient(endpoint, api_key, args.workspace_slug)
result = execute_action(client, args)
print(result if isinstance(result, str) else to_json(result))
return 0
except Exception as exc:
message = str(exc) if str(exc) else type(exc).__name__
print(to_json({"error": message}), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())