From memstack
Audits Supabase Row Level Security policies across all tables by scanning migrations, database types, client code, and storage buckets. Flags missing RLS and intentional skips.
npx claudepluginhub cwinvestments/memstack --plugin memstackThis skill uses the workspace's default tool permissions.
*Audit Supabase Row Level Security policies across all tables in a project.*
Enforces Row Level Security policies on Supabase and PostgreSQL tables during CREATE TABLE, ALTER TABLE, migrations, or RLS discussions. Provides 4 policy patterns, templates, checks, and mistake avoidance.
Provides expertise in securing Supabase applications: RLS patterns, auth token validation, storage security, multi-tenant isolation. Grounds responses in patterns.md, sharp_edges.md, validations.md for creation, diagnosis, review.
Applies Supabase security best practices: anon/service_role key separation, RLS enforcement, JWT verification, API hardening. Use for securing projects, auditing keys, or production checklists.
Share bugs, ideas, or general feedback.
Audit Supabase Row Level Security policies across all tables in a project.
When this skill activates, output:
π RLS Checker β Auditing Row Level Security...
Then execute the protocol below.
| Context | Status |
|---|---|
| User asks to check/audit RLS | ACTIVE β full audit |
| User mentions Supabase security | ACTIVE β full audit |
| User asks about table permissions | ACTIVE β full audit |
| User is writing RLS policies | DORMANT β they know what they're doing |
| Non-Supabase project | DORMANT β not applicable |
Find all Supabase tables referenced in the project. Search in priority order:
Migration files β most authoritative source:
find . -path "*/migrations/*.sql" -o -path "*/supabase/migrations/*.sql" | head -50
Look for CREATE TABLE statements.
Generated types β comprehensive if available:
types/database.ts, types/supabase.ts, src/types/database.types.ts, database.types.ts
Parse the Tables interface for all table names.
Client usage β catches tables missed by above:
grep -r "\.from(['\"]" --include="*.ts" --include="*.tsx" --include="*.js"
Extract table names from .from('table_name') calls.
Storage buckets β separate RLS surface:
grep -r "storage\.from\|createBucket\|storage-api" --include="*.ts" --include="*.tsx" --include="*.sql"
Compile a deduplicated list of all tables and storage buckets.
For each table, find its RLS configuration:
Search migration SQL for RLS statements:
ALTER TABLE <name> ENABLE ROW LEVEL SECURITY β RLS is onCREATE POLICY statements β extract policy name, operation (SELECT/INSERT/UPDATE/DELETE/ALL), and USING/WITH CHECK expressionsALTER TABLE <name> FORCE ROW LEVEL SECURITY β RLS enforced even for table ownersCheck for intentionally unprotected tables:
GRANT SELECT ON <table> TO anon without RLS are intentionally public-- public table, -- no RLS needed, or -- rls:skip in migration SQL-- rls:skip should be classified as β
OK (Intentional) in the report, not flagged as missing RLS. This lets teams explicitly document tables that rely on application-level authorization (e.g., service-role-first architectures).-- rls:skip marker exists and no RLS is enabled, flag normally.Check Supabase dashboard-configured policies:
supabase inspect db policies or the Dashboard UI."supabase/config.toml, .supabase/) that might indicate whether the project uses Dashboard-managed policies.supabase db dump --schema public --data-only=false | grep -A5 "CREATE POLICY"
Check Supabase dashboard seed/init files for policy definitions that may not be in migrations.
For each table with RLS enabled, evaluate policy quality:
Check 1 β Operation Coverage: Flag tables missing policies for any CRUD operation:
Check 2 β User Isolation: Verify policies filter by authenticated user:
auth.uid() in USING clause β standard user isolation (OK)auth.uid() in WITH CHECK clause β write isolation (OK)auth.uid() reference β overly permissive (WARNING)auth.uid() β security risk (CRITICAL)current_setting('app.*') instead of auth.uid() β anti-pattern (WARNING). This relies on the application explicitly setting a PostgreSQL session variable before every query. If the variable is unset, the policy may fail open or closed unpredictably. Prefer auth.uid() which Supabase populates automatically from the JWT. Flag with:
grep -rn "current_setting" --include="*.sql"
Check 3 β Multi-Tenant Isolation:
For tables with organization_id or team_id columns:
organization_id = <value> without membership check is insufficient (WARNING)CREATE POLICY "org_isolation" ON documents
USING (organization_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));
Check 4 β Service Role Bypass: Search codebase for service role usage that bypasses RLS:
grep -r "service_role\|serviceRole\|supabaseAdmin\|SUPABASE_SERVICE_ROLE" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.env*"
.env committed to git β critical vulnerability (CRITICAL)Check 5 β Storage Bucket Policies: For each storage bucket found:
auth.uid()::text = (storage.foldername(name))[1])If the project uses service role for most/all database access (Check 4 found widespread supabaseAdmin / SUPABASE_SERVICE_ROLE usage), compute a defense-in-depth score:
Score calculation:
(tables with RLS / total sensitive tables) Γ 100Classification:
| Score | Rating | Meaning |
|---|---|---|
| 80β100% | π’ Strong | RLS provides meaningful backup even though service role bypasses it |
| 50β79% | π‘ Partial | Some defense-in-depth but gaps remain |
| 20β49% | π Weak | Most sensitive tables unprotected at DB layer |
| 0β19% | π΄ None | Entire security model depends on application code β single bug = full breach |
Include in report:
## Defense-in-Depth Score
Architecture: Service-role-first (all API routes use service role key)
Sensitive tables: <count>
Sensitive tables with RLS: <count>
Score: <percentage> β <rating>
Note: Service role bypasses RLS by design. This score measures how well
the database would protect data if an application-level auth bug occurred.
Recommendation for low scores: Even in service-role architectures, enabling RLS on sensitive tables provides a safety net. If a developer accidentally uses the anon key, creates a new route without auth, or a future refactor introduces a bug, RLS prevents cross-tenant data access at the database layer.
Output a structured report with this format:
π RLS Audit Report
Project: <project-name>
Tables found: <count>
Storage buckets: <count>
## Table Audit
| Table | RLS | Policies | Coverage | Risk | Issue |
|-------|-----|----------|----------|------|-------|
| users | ON | 4 | Full | β
OK | β |
| documents | ON | 2 | Partial | β οΈ WARN | Missing DELETE policy |
| payments | OFF | 0 | None | π΄ CRIT | No RLS enabled |
| public_posts | OFF | 0 | N/A | β
OK | Intentionally public (-- rls:skip) |
## Storage Buckets
| Bucket | Policies | Risk | Issue |
|--------|----------|------|-------|
| avatars | 2 | β
OK | β |
| uploads | 0 | β οΈ WARN | No upload restriction |
## Critical Issues
1. **payments** β No RLS enabled. Any authenticated user can read/write all rows.
β Fix: `ALTER TABLE payments ENABLE ROW LEVEL SECURITY;` then add user-scoped policies.
2. **service_role in client** β Found in `src/lib/supabase.ts:14`.
β Fix: Remove service role key from client code. Use server-side API route instead.
## Warnings
1. **documents** β Missing DELETE policy. Users may not be able to delete their own documents, or deletion may be unrestricted.
β Fix: Add `CREATE POLICY "delete_own" ON documents FOR DELETE USING (user_id = auth.uid());`
2. **uploads bucket** β No storage policies defined.
β Fix: Add bucket policies restricting uploads to user-specific paths.
## Summary
- π΄ Critical: <count>
- β οΈ Warning: <count>
- β
OK: <count>
- Total tables: <count>
For each CRITICAL and WARNING issue, provide:
Offer to generate a migration file with all fixes: supabase/migrations/<timestamp>_rls_fixes.sql
| Level | Meaning | Action |
|---|---|---|
| π΄ CRITICAL | Data exposed or writable by unauthorized users | Fix immediately |
| β οΈ WARNING | Incomplete coverage or weak isolation | Fix before production |
| βΉοΈ INFO | Acceptable pattern that should be verified | Review and confirm intentional |
| β OK | Properly secured | No action needed |
User-owned rows:
CREATE POLICY "users_own_data" ON table_name
FOR ALL USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
Org-scoped with membership check:
CREATE POLICY "org_members_access" ON table_name
FOR ALL USING (
organization_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
)
);
Public read, authenticated write:
CREATE POLICY "public_read" ON table_name FOR SELECT USING (true);
CREATE POLICY "auth_insert" ON table_name FOR INSERT WITH CHECK (auth.role() = 'authenticated');
Storage bucket user isolation:
CREATE POLICY "user_uploads" ON storage.objects
FOR INSERT WITH CHECK (
bucket_id = 'uploads' AND
auth.uid()::text = (storage.foldername(name))[1]
);
current_setting() anti-pattern detection (vs auth.uid()), -- rls:skip marker for intentionally unprotected tables, Supabase Dashboard policy detection guidance (supabase inspect db policies), defense-in-depth score for service-role-first architectures. (Origin: AdminStack audit, Mar 2026)