skills/supabase-admin/SKILL.md
Supabase administration, RLS policies, migrations, and schema design. Use for database architecture, Row Level Security, performance tuning, auth integration. Activate on "Supabase", "RLS", "migration", "policy", "schema", "auth.uid()". NOT for Supabase Auth UI configuration (use dashboard), edge functions (use cloudflare-worker-dev), or general SQL without Supabase context.
npx skillsauth add curiositech/windags-skills supabase-adminInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Master Supabase schema design, Row Level Security policies, migrations, and performance optimization for production applications.
Check data sensitivity & access patterns:
├── Public content (blogs, docs)
│ └── Use: Public read + owner write policies
├── User-owned data (profiles, settings)
│ └── Use: Owner-only access (auth.uid() = user_id)
├── Multi-tenant (org data)
│ ├── Small orgs (<1000 users): RLS with org_id filter
│ └── Large orgs (>1000 users): Schema per tenant
└── Admin content (system config)
└── Use: Role-based policies with profiles.role check
Evaluate data retention needs:
├── Needs audit trail or recovery?
│ ├── High compliance: Soft delete (deleted_at timestamp)
│ └── Medium compliance: Soft delete + cascade trigger
├── Performance critical with large volume?
│ ├── Yes: Hard delete with CASCADE
│ └── No: Soft delete acceptable
└── Referenced by other tables?
├── Critical refs: Use ON DELETE RESTRICT
└── Clean cascade: Use ON DELETE CASCADE
Check change impact:
├── Schema changes
│ ├── Adding nullable column: Low risk, deploy anytime
│ ├── Adding NOT NULL: Medium risk, needs backfill
│ └── Dropping column: High risk, needs staged deployment
├── RLS policy changes
│ ├── Relaxing permissions: Low risk
│ └── Restricting access: High risk, test with real data
└── Index changes
├── Creating index: Use CONCURRENTLY flag
└── Dropping index: Check query performance impact first
Symptoms: Tables with 20+ columns, nullable columns everywhere, poor query performance
Detection: SELECT count(*) FROM information_schema.columns WHERE table_name = 'bloated_table' returns >15
Fix: Normalize into related tables, use JSONB for flexible attributes, add NOT NULL constraints with defaults
Symptoms: Queries taking >1000ms, high CPU on simple selects, missing auth.uid() in WHERE clauses
Detection: EXPLAIN ANALYZE shows sequential scans on user_id columns without indexes
Fix: Add CREATE INDEX idx_table_user_id ON table(user_id) for every RLS-filtered column
Symptoms: Deployment timeouts, blocked writes during migration, "relation does not exist" errors
Detection: Migration contains ALTER TABLE ADD COLUMN x NOT NULL without DEFAULT
Fix: Split into: 1) Add nullable column with default, 2) Backfill data, 3) Add NOT NULL constraint
Symptoms: RLS policies calling auth.uid() for every row, poor performance on large tables
Detection: Policy has auth.uid() = user_id in USING clause without subquery
Fix: Rewrite as user_id = (SELECT auth.uid()) to parse JWT once per query
Symptoms: Data disappearing unexpectedly, orphaned records, referential integrity errors Detection: Foreign keys without explicit ON DELETE behavior, surprise empty result sets Fix: Explicitly set ON DELETE CASCADE/RESTRICT/SET NULL based on business logic
Scenario: SaaS app where users belong to organizations, need org-isolated data
Step 1: Schema Design Decision
-- Decision: Use org_id column vs separate schemas
-- Trade-off: RLS filtering vs complete isolation
-- Choice: RLS (easier ops, shared resources)
CREATE TABLE posts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid NOT NULL REFERENCES organizations(id),
user_id uuid NOT NULL REFERENCES auth.users(id),
content text NOT NULL,
created_at timestamptz DEFAULT now()
);
Step 2: Index Strategy (Critical for Performance)
-- Compound index: org_id first (highest selectivity for RLS)
CREATE INDEX idx_posts_org_user ON posts(org_id, user_id);
-- Single index for user lookups
CREATE INDEX idx_posts_user ON posts(user_id);
Step 3: RLS Policy with Org Context
-- Get user's org_id from profiles table
CREATE POLICY "Org members only" ON posts
FOR ALL USING (
org_id = (
SELECT profiles.org_id
FROM profiles
WHERE profiles.id = auth.uid()
)
);
Step 4: Performance Validation
-- Test query performance
EXPLAIN ANALYZE
SELECT * FROM posts
WHERE org_id = 'test-org-id'
LIMIT 10;
-- Should show Index Scan, not Seq Scan
-- Should be <100ms for tables with <1M rows
Expert Insight: Novices miss the compound index ordering. They create (user_id, org_id) which forces RLS to scan all user posts then filter by org. Experts put org_id first since RLS filters by org membership.
RLS audit checklist - all must pass before production:
ALTER TABLE x ENABLE ROW LEVEL SECURITYSET ROLE anon and real user JWTsEXPLAIN ANALYZE shows Index Scan (not Seq Scan) for auth queriesALTER TABLE ADD COLUMN x NOT NULL without DEFAULT in single transactionDo NOT use this skill for:
cloudflare-worker-dev skill insteadpostgresql-dba skill for non-Supabase contextWhen to delegate:
postgresql-dba skilldocker-deployment or kubernetes-ops skillstools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.