Skip to content

Instantly share code, notes, and snippets.

@martinbowling
Last active May 2, 2026 20:46
Show Gist options
  • Select an option

  • Save martinbowling/c905852a8be1c1ac84d5ff6fc0d328ee to your computer and use it in GitHub Desktop.

Select an option

Save martinbowling/c905852a8be1c1ac84d5ff6fc0d328ee to your computer and use it in GitHub Desktop.
my saas skill.md for my agent doing supabase work
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.

Supabase Skill — Social Media Monitoring SaaS

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.

Operating mode

  • 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.

Authentication & Personal Access Tokens

Three keys, three jobs — don't mix them up

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.

PAT handling rules

  1. 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.
  2. Never in code, never in repos. Load from secret manager (1Password, Doppler, Infisical, AWS Secrets Manager). If you ever see a PAT in a .env committed to git, rotate immediately.
  3. Never in logs. Strip from every log line. Add a regex filter: sbp_[a-f0-9]{40,}[REDACTED_PAT].
  4. 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.
  5. 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.
  6. Use organization-scoped PATs only when you must. Prefer project-scoped access. Org PATs can spin up new projects and rack up bills.

How to use the PAT in practice

# 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=1

If 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.

Schema patterns for SMM data

Multi-tenancy: workspace_id on everything

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);

Time-series tables get partitioned

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.

JSONB for platform-specific metadata

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.

pgvector for semantic search

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);

Realtime for live mention feeds

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.

Security audit checklist

Run this whole list on every project, monthly. The agent should be able to execute it autonomously and post a report.

RLS

  • Every table in the public schema 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_policies and flag tables with RLS on but zero policies.
  • Policies use (select auth.uid()) not auth.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())
    )
  );

SECURITY DEFINER functions

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 INVOKER unless you have a written reason not to.

Public schema exposure

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;

Auth config

  • Email confirmations on for production.
  • Password minimum length ≥ 12, with leaked-password protection enabled.
  • MFA available, and enforced for users with the admin role on a workspace.
  • JWT expiry ≤ 1 hour, refresh token rotation on.
  • Site URL and redirect URLs locked to your actual domains. No wildcards. No localhost in prod.
  • Rate limits on signup, password reset, OTP. Default Supabase limits are too generous for a B2B tool.

Storage buckets

  • 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

  • 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.

Extensions

  • Audit which extensions are enabled: select * from pg_extension;
  • No extensions you don't recognize. pg_net and http give the database outbound network access — useful for webhooks, dangerous if misused.
  • pgcrypto and vector should be there. pg_stat_statements for monitoring.

Performance audit

Indexes

  • 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_indexes to find unused indexes too — they cost write performance.

Query plans

  • EXPLAIN ANALYZE any 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 on mentions = missing index.

Connection pooling

  • 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_timeout per role. Anon role gets aggressive timeouts (5s). Service role gets longer (30s).

Vacuum and bloat

  • pg_stat_user_tables — flag tables where n_dead_tup is more than 20% of n_live_tup. Tune autovacuum on hot tables (mentions, engagement_events).

Operational best practices

Migrations

  • 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."

Backups and PITR

  • 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.

Edge functions

  • 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.

Scheduled jobs

  • Use pg_cron for 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_log with start time, end time, status, error message.

Monitoring

  • 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.

Agent audit log

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.

Things to never do

  • 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 a where clause — even with RLS, even on staging. Always add where and a limit first 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.content with a regular btree. It's a long text column. Use tsvector + 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.

Standard workflow

When asked to perform any Supabase operation:

  1. Confirm scope — which project, which workspace, what's the intended outcome.
  2. Check current state — read first. RLS, existing schema, current data shape.
  3. Plan the change — describe what you're about to do in one paragraph.
  4. Get confirmation for anything destructive or production-affecting.
  5. Execute on a branch if it's a schema or auth change; merge after verification.
  6. Log to agent_audit_log.
  7. Report back with a one-line summary, what was done, and how to roll it back if needed.

Quick reference: Management API endpoints used most

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.

When in doubt

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment