| name | supabase |
|---|---|
| description | Use when the agent needs to work with Supabase for the social media monitoring platform — provisioning projects, running migrations, auditing RLS, debugging ingestion pipelines, configuring auth, managing edge functions, querying mentions/alerts data, or troubleshooting performance on high-volume tables (mentions, posts, engagement events). Triggers on any mention of Supabase, Postgres, RLS, PostgREST, pg_cron, pgvector, edge functions, or when working with the platform's database, auth, storage, or realtime features. |
You operate Supabase for an SMM platform. That means high-write ingestion (mentions, posts, comments), multi-tenant workspaces, PII inside monitored content, and customers who notice the second a webhook drops. Treat the database like production infrastructure, not a notebook.
- Default to read-only. Inspect before you mutate. Run
EXPLAIN,SELECT count(*), check RLS, then act. - Confirm destructive ops. Anything that drops, truncates, alters a column type, disables RLS, or modifies auth config gets explicit confirmation in chat before execution.
- Use branches for schema changes. Never run migrations directly against
production. Spin a dev branch, run the migration, verify, merge. - Stay scoped. If the user asks about workspace
acme, don't query across all tenants. Filter early. - Log everything you do. Append a one-line summary to
agent_audit_log(see schema below) for every state-changing call.
| Credential | What it does | Where it lives |
|---|---|---|
| Personal Access Token (PAT) | Calls the Management API — creates projects, runs migrations, manages branches, reads logs | Agent's secret store, SUPABASE_PAT env var |
| Service role key | Bypasses RLS, full DB access via PostgREST | Server-side only, SUPABASE_SERVICE_ROLE_KEY |
| Anon key | Public, RLS-enforced PostgREST access | Client-side, fine to ship in browser |
A PAT is not a database credential. It can't query your tables. It manages the project. Don't try to use it for data ops — use a service role key or a scoped DB role instead.
- Scope per agent. Generate one PAT per agent identity. Name it descriptively:
agent-foreman-prod-2026q2. When something goes wrong, you want to know which PAT to revoke. - Never in code, never in repos. Load from secret manager (1Password, Doppler, Infisical, AWS Secrets Manager). If you ever see a PAT in a
.envcommitted to git, rotate immediately. - Never in logs. Strip from every log line. Add a regex filter:
sbp_[a-f0-9]{40,}→[REDACTED_PAT]. - Rotate every 90 days. Set a calendar reminder. Old PAT stays valid for 24 hours after rotation to give in-flight jobs a grace window, then revoke.
- Never echo back. If a user pastes a PAT into chat, do not repeat it, do not store it in conversation memory, and tell them to rotate it because the chat transcript now contains it.
- Use organization-scoped PATs only when you must. Prefer project-scoped access. Org PATs can spin up new projects and rack up bills.
# Management API — uses PAT
curl -H "Authorization: Bearer $SUPABASE_PAT" \
https://api.supabase.com/v1/projects/$PROJECT_REF
# PostgREST — uses service_role or anon, NEVER the PAT
curl -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \
-H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \
https://$PROJECT_REF.supabase.co/rest/v1/mentions?select=id&limit=1If you're using the Supabase MCP server, run it in --read-only mode by default and pass --project-ref to scope it to one project.
Every customer-facing table gets workspace_id uuid not null with an FK to workspaces(id) and an index. RLS is built on this column. No exceptions.
create table mentions (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references workspaces(id) on delete cascade,
platform text not null check (platform in ('twitter','reddit','tiktok','instagram','youtube','news','blog')),
external_id text not null,
author_handle text,
content text not null,
posted_at timestamptz not null,
ingested_at timestamptz not null default now(),
sentiment numeric(4,3),
metadata jsonb not null default '{}'::jsonb,
unique (platform, external_id)
);
create index mentions_workspace_posted on mentions (workspace_id, posted_at desc);
create index mentions_metadata_gin on mentions using gin (metadata jsonb_path_ops);mentions, engagement_events, and api_usage_events grow by millions of rows a week. Use pg_partman or native declarative partitioning by posted_at monthly. Set retention via partman config — most plans should drop raw mentions after 13 months and keep aggregates forever.
Each platform has different fields (Twitter has retweet_count, Reddit has upvote_ratio). Don't make 40 nullable columns. Use metadata jsonb with a GIN index. Document the expected keys per platform in a comment on the column.
Enable vector extension. Add embedding vector(1536) (or whatever your model dimension is) to mentions. Use HNSW index, not IVFFlat — better recall at this scale.
create index mentions_embedding_hnsw on mentions
using hnsw (embedding vector_cosine_ops)
with (m = 16, ef_construction = 64);Enable the supabase_realtime publication on mentions and alerts. Do not enable it on engagement_events — that table writes too fast and will saturate the realtime broadcaster.
Run this whole list on every project, monthly. The agent should be able to execute it autonomously and post a report.
- Every table in the
publicschema has RLS enabled. Query:select schemaname, tablename from pg_tables where schemaname='public' and rowsecurity=false;— should return zero rows. If it doesn't, that's a P0. - Every table has at least one policy. RLS enabled with no policies = nothing readable, which is its own kind of bug. Query
pg_policiesand flag tables with RLS on but zero policies. - Policies use
(select auth.uid())notauth.uid(). The subquery form lets Postgres cache the value per query — massive perf difference at scale. - Workspace policies check membership, not ownership. Customers add team members; a
created_by = auth.uid()policy locks teammates out.
-- Good: workspace membership check
create policy mentions_workspace_read on mentions
for select using (
workspace_id in (
select workspace_id from workspace_members
where user_id = (select auth.uid())
)
);These run with the creator's privileges, bypassing RLS. They're the #1 source of Supabase data leaks.
- List them all:
select n.nspname, p.proname, pg_get_userbyid(p.proowner) as owner from pg_proc p join pg_namespace n on n.oid=p.pronamespace where p.prosecdef = true; - Every SECURITY DEFINER function must
set search_path = ''(or to a specific safe path) to prevent search_path injection. - Every SECURITY DEFINER function must do its own authorization check — typically an
auth.uid()check followed by a workspace membership lookup. - Default to
SECURITY INVOKERunless you have a written reason not to.
Anything in the public schema is reachable by PostgREST. If it's internal (job queue, audit log, raw scrape data), put it in a separate schema not exposed via the API.
create schema if not exists internal;
revoke all on schema internal from anon, authenticated;Then move tables: alter table jobs set schema internal;
- Email confirmations on for production.
- Password minimum length ≥ 12, with leaked-password protection enabled.
- MFA available, and enforced for users with the
adminrole on a workspace. - JWT expiry ≤ 1 hour, refresh token rotation on.
- Site URL and redirect URLs locked to your actual domains. No wildcards. No
localhostin prod. - Rate limits on signup, password reset, OTP. Default Supabase limits are too generous for a B2B tool.
- No public buckets unless you've thought hard about it. Logos and avatars maybe. Customer report PDFs and exported mention archives, never.
- Storage policies mirror table RLS — check workspace membership, not ownership.
- File size and MIME type limits set per bucket.
- Realtime subscriptions respect RLS. Test it: subscribe as user A, verify you don't see workspace B's mentions.
- Don't broadcast tables with PII unless RLS is bulletproof. Mention content qualifies.
- Audit which extensions are enabled:
select * from pg_extension; - No extensions you don't recognize.
pg_netandhttpgive the database outbound network access — useful for webhooks, dangerous if misused. -
pgcryptoandvectorshould be there.pg_stat_statementsfor monitoring.
- Every foreign key has an index. Postgres does not auto-create them. Query: find FKs without supporting indexes (there's a standard query for this — run it monthly).
- Every RLS policy's lookup column is indexed. If your policy filters on
workspace_id, that column needs an index on every table. - No duplicate indexes. Use
pg_stat_user_indexesto find unused indexes too — they cost write performance.
-
EXPLAIN ANALYZEany query that the dashboard runs more than 1000x/day. Anything over 100ms gets attention. - Watch for sequential scans on
mentions— that table is too big for that, ever. Sequential scan onmentions= missing index.
- Edge functions and serverless workers connect via the transaction-mode pooler (port 6543), not direct (port 5432).
- Long-lived workers (ingestion daemons) use session mode or direct connection.
- Set
statement_timeoutper role. Anon role gets aggressive timeouts (5s). Service role gets longer (30s).
-
pg_stat_user_tables— flag tables wheren_dead_tupis more than 20% ofn_live_tup. Tune autovacuum on hot tables (mentions,engagement_events).
- Every schema change goes through a migration file in version control. No clicking around the Supabase dashboard for prod changes — that's how you end up with undocumented columns.
- Use Supabase CLI:
supabase migration new add_mention_sentiment. - Test on a branch:
supabase branches create staging-sentiment-feature. Run the migration. Run the test suite. Merge. - Migrations must be reversible or forward-only with a documented rollback strategy. No "we'll figure it out if it breaks."
- Confirm Point-In-Time Recovery is enabled on the prod project. Without PITR you only get daily backups, which is not enough for a customer-facing system.
- Quarterly: do a real recovery drill. Restore to a branch, verify data, document time-to-recover.
- Secrets via
supabase secrets set, not hardcoded. Deno has access to env vars. - Verify webhook signatures (Twitter/X, Slack, etc.) before processing — a webhook endpoint is a public endpoint.
- Rate limit per workspace at the edge function layer, not just at the database.
- Set CPU/memory/timeout limits explicitly. Default 60s timeout is too long for most ingestion jobs.
- Use
pg_cronfor in-database scheduled work (cleanup, aggregation, partition rotation). - Use external schedulers (GitHub Actions, Cloudflare Cron) for jobs that hit external APIs (platform scrapes). Don't make Postgres responsible for HTTP retries against rate-limited APIs.
- Every cron job logs to
cron_run_logwith start time, end time, status, error message.
- Hook up the project to your alerting (PagerDuty, OpsGenie, whatever).
- Critical alerts: connection pool exhaustion, replication lag (if using read replicas), disk usage > 80%, auth failure rate spike.
- Customer-facing SLO: p95 mention API latency < 200ms. If it climbs, page someone.
Every state-changing operation the agent performs gets logged. Schema:
create table internal.agent_audit_log (
id bigserial primary key,
agent_id text not null,
ts timestamptz not null default now(),
operation text not null,
target text,
workspace_id uuid,
pat_fingerprint text, -- last 4 chars of PAT used, never the full token
request_summary text,
result text check (result in ('success','error','aborted')),
error_detail text
);The agent inserts here before and after every mutation. If the agent crashes mid-operation, you can see what was attempted.
- Disable RLS on a table that ever had it on. Even temporarily. Even for "just a quick query." If you need to bypass RLS, use the service role key or
set local role, then revert. - Run
delete from <table>without awhereclause — even with RLS, even on staging. Always addwhereand alimitfirst as a sanity check. - Hand out service role keys to client applications. That's the entire RLS bypass key. It belongs server-side, full stop.
- Store PATs or service role keys in agent memory or conversation history. They go in the secret store, get loaded at runtime, and never appear in transcripts.
- Auto-confirm destructive Management API calls like project deletion, branch deletion, or storage bucket deletion. Always require a typed confirmation.
- Run untested migrations on production directly. Branch first.
- Index
mentions.contentwith a regular btree. It's a long text column. Usetsvector+ GIN for full-text, or pgvector for semantic. - Trust user input in raw SQL. Use parameterized queries everywhere. PostgREST handles this; if you drop down to direct SQL via the Management API or a function, parameterize.
When asked to perform any Supabase operation:
- Confirm scope — which project, which workspace, what's the intended outcome.
- Check current state — read first. RLS, existing schema, current data shape.
- Plan the change — describe what you're about to do in one paragraph.
- Get confirmation for anything destructive or production-affecting.
- Execute on a branch if it's a schema or auth change; merge after verification.
- Log to
agent_audit_log. - Report back with a one-line summary, what was done, and how to roll it back if needed.
| Endpoint | Use |
|---|---|
GET /v1/projects |
List projects on the org |
GET /v1/projects/{ref} |
Project details |
POST /v1/projects/{ref}/database/query |
Run SQL via Management API |
GET /v1/projects/{ref}/branches |
List branches |
POST /v1/projects/{ref}/branches |
Create a branch |
GET /v1/projects/{ref}/secrets |
List edge function secrets (names only) |
POST /v1/projects/{ref}/secrets |
Set edge function secrets |
GET /v1/projects/{ref}/postgrest |
PostgREST config |
GET /v1/projects/{ref}/config/auth |
Auth config |
Always set Authorization: Bearer $SUPABASE_PAT and Content-Type: application/json.
If the agent is uncertain whether an operation is safe, the answer is: stop, summarize, ask the human. The cost of a confirmation prompt is five seconds. The cost of dropping a tenant's mentions table is the company.