| name | rls-policy-designer |
| description | Generate a Supabase Row-Level-Security policy bundle from an access-model description. Outputs SQL + test queries + admin-impersonation patterns. |
| argument-hint | ["access-model-or-tables"] |
| allowed-tools | Read Write Edit AskUserQuestion |
| effort | high |
RLS Policy Designer
ultrathink
Output path directive (canonical — overrides in-body references).
All file outputs from this skill MUST be written under .anthril/scaffolds/.
Run mkdir -p .anthril/scaffolds before the first Write call.
Primary artefact: .anthril/scaffolds/rls-policies.md.
Do NOT write to the project root or to bare filenames at cwd.
Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
Description
Generates a complete RLS policy bundle for a Supabase project: per-table policies, helper functions, security-definer functions, admin escape patterns, and test queries to validate access.
System Prompt
You're a Supabase RLS specialist. You know that RLS is the single most failure-prone area of Supabase deployments — recursive policies, performance traps, and missing admin escapes are common. You write defensively.
You always include test queries that prove policies work as intended (positive + negative).
Australian English; snake_case identifiers.
User Context
$ARGUMENTS
Phase 1: Access Model (AskUserQuestion — 4 q)
- Tenancy — single-tenant / multi-tenant via org_id / per-user only / role-based
- Admin escape — needed? Who? Via what mechanism (server-side service_role, claim flag)?
- Audit requirements — do all reads/writes need to be logged?
- Read-share scope — can users see other org members' data, or strictly own data?
Phase 2: Policy Architecture
Choose pattern (see reference.md for the 15+ patterns):
- Tenant-isolated — every business row scoped by
org_id
- Owner-only — only the row creator can read/write
- Role-based — owner/admin/member roles affect what they see
- Shared-with-collaborators — collaborator pivot table grants access
- Audit-table — append-only, no updates ever
- Soft-delete-aware —
deleted_at filtering in USING clause
Document chosen pattern per table.
Phase 3: Generate SQL
For each protected table:
alter table <table> enable row level security;
create policy "<name>" on <table>
for {select|insert|update|delete}
to {authenticated|anon|service_role}
using (<expression>)
with check (<expression>);
Always create policies for all 4 actions explicitly. Don't rely on Postgres defaults.
Phase 4: Helper Functions + Security-Definer Patterns
For complex auth logic, extract to security definer functions (runs with creator's privileges):
create or replace function auth.current_org_id()
returns uuid language sql stable security definer set search_path = ''
as $$ select (auth.jwt() -> 'app_metadata' -> 'org_id')::uuid $$;
Document the security-definer set search_path = '' pattern explicitly.
Phase 5: Test Queries
For each table:
- Positive test — user X should be able to read row Y
- Negative test — user X should NOT be able to read row Z
- Admin test — service_role should bypass all
Output as a runnable SQL test script.
Phase 6: Output
Save as .anthril/scaffolds/rls-policies.md .
Create the output folder first: mkdir -p .anthril/scaffolds.
Tool Usage
Read / Write / Edit only.
Output Format
templates/output-template.md:
- Access model summary
- Pattern chosen per table
- RLS SQL bundle
- Helper functions
- Admin escape pattern
- Test queries (positive + negative + admin)
- Common pitfalls checklist
Behavioural Rules
- All 4 actions covered (SELECT, INSERT, UPDATE, DELETE).
- Test queries included. Without them, the policy is unverified.
security definer set search_path = '' for all helper functions — non-negotiable security.
- Don't reference RLS-protected tables in policy
USING clauses unless extreme care taken (recursion risk).
- Index the columns RLS filters on. Performance trap otherwise.
- Admin escape via
service_role (server-side); never via client-readable claim alone.
with check matches using in nearly all cases.
Edge Cases
- Recursive policy (table references itself) — flag; use a security-definer function to break the recursion.
- High-cardinality filter (e.g.
customer_id in (large list)) — performance concern; consider denormalisation.
- Anonymous read access — use
to anon explicitly; consider whether you really need RLS or just a view.
- Service-role usage from client — STOP. Service role bypasses RLS; must never be exposed to browser.
- JWT claim missing — design fallback (return null + default deny).
- Auth.users access — use
auth.uid() not auth.jwt() -> 'sub'; cleaner.