skills/stack/supabase-rls-patterns/SKILL.md
# Supabase RLS Patterns Row Level Security (RLS) is the primary authorization mechanism in Supabase. Every table exposed to PostgREST MUST have RLS enabled with explicit policies. There is no "default deny" without enabling RLS — without it, all rows are visible. --- ## Enabling RLS Every table MUST have RLS enabled in the same migration that creates it: ```sql ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; ``` **No exceptions.** A table without RLS is fully readable and writable by any a
npx skillsauth add 33prime/rtg-forge skills/stack/supabase-rls-patternsInstall 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.
Row Level Security (RLS) is the primary authorization mechanism in Supabase. Every table exposed to PostgREST MUST have RLS enabled with explicit policies. There is no "default deny" without enabling RLS — without it, all rows are visible.
Every table MUST have RLS enabled in the same migration that creates it:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
No exceptions. A table without RLS is fully readable and writable by any authenticated user.
Supabase provides helper functions to access the authenticated user's identity:
-- Get the user's UUID (from the JWT sub claim)
auth.uid()
-- Get the full JWT payload (for custom claims like tenant_id, role)
auth.jwt()
-- Extract a specific claim
(auth.jwt() ->> 'tenant_id')::uuid
(auth.jwt() ->> 'role')::text
For multi-tenant apps, set custom claims in the JWT (via Supabase Auth hooks or custom token):
{
"sub": "user-uuid",
"tenant_id": "tenant-uuid",
"role": "admin",
"app_metadata": {
"permissions": ["invoices:read", "invoices:write"]
}
}
For tables where each row belongs to a single user:
CREATE POLICY users_own_data ON user_profiles
FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
For multi-tenant tables — the most common pattern:
CREATE POLICY tenant_isolation ON invoices
FOR ALL
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid)
WITH CHECK (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);
For tables where access depends on the user's role:
-- Anyone in the tenant can read
CREATE POLICY invoices_select ON invoices
FOR SELECT
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::uuid);
-- Only admins and managers can insert/update
CREATE POLICY invoices_modify ON invoices
FOR INSERT
WITH CHECK (
tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
AND (auth.jwt() ->> 'role')::text IN ('admin', 'manager')
);
CREATE POLICY invoices_update ON invoices
FOR UPDATE
USING (
tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
AND (auth.jwt() ->> 'role')::text IN ('admin', 'manager')
)
WITH CHECK (
tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
);
For child tables that inherit access from their parent:
CREATE POLICY line_items_via_invoice ON line_items
FOR ALL
USING (
EXISTS (
SELECT 1 FROM invoices
WHERE invoices.id = line_items.invoice_id
AND invoices.tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM invoices
WHERE invoices.id = line_items.invoice_id
AND invoices.tenant_id = (auth.jwt() ->> 'tenant_id')::uuid
)
);
For tables like product catalogs:
CREATE POLICY products_public_read ON products
FOR SELECT
USING (published = true);
CREATE POLICY products_admin_write ON products
FOR ALL
USING ((auth.jwt() ->> 'role')::text = 'admin')
WITH CHECK ((auth.jwt() ->> 'role')::text = 'admin');
Use a consistent naming scheme:
<table>_<operation>_<who>
Examples:
invoices_select_tenant — Tenant members can read invoicesinvoices_insert_admin — Only admins can create invoicesline_items_all_via_invoice — All operations via parent invoice checkproducts_select_public — Anyone can read published products| Clause | Applies To | Purpose |
|---|---|---|
| USING | SELECT, UPDATE (existing rows), DELETE | Filters which existing rows are visible |
| WITH CHECK | INSERT, UPDATE (new row values) | Validates the new/modified row |
Always specify both for INSERT and UPDATE policies to prevent:
tenant_id on existing rowsThe service_role key bypasses RLS entirely. Use it ONLY for:
# Python — service role client for admin operations
admin_client = create_client(
supabase_url=settings.supabase_url,
supabase_key=settings.supabase_service_role_key, # Bypasses RLS!
)
Never expose the service_role key to the frontend.
Use SECURITY DEFINER with extreme caution. It runs the function as the function owner (usually the postgres superuser), bypassing RLS.
-- DANGEROUS — this function bypasses all RLS
CREATE OR REPLACE FUNCTION get_all_invoices()
RETURNS SETOF invoices
LANGUAGE sql
SECURITY DEFINER
AS $$
SELECT * FROM invoices;
$$;
search_path to prevent injection:
SET search_path = public, pg_temp;
Always test your policies by simulating different user contexts:
-- Simulate an authenticated user
SET request.jwt.claims = '{"sub": "user-1", "tenant_id": "tenant-1", "role": "member"}';
SET role = 'authenticated';
-- This should return only tenant-1 invoices
SELECT * FROM invoices;
-- This should fail (wrong tenant)
INSERT INTO invoices (tenant_id, customer_id, status)
VALUES ('tenant-2', 'customer-1', 'draft');
-- Reset
RESET role;
RESET request.jwt.claims;
development
# Parallel Execution > This skill is under development. Workflow patterns for running independent tasks in parallel to improve performance and throughput. ## Topics to Cover - Identifying independent tasks suitable for parallel execution - `asyncio.gather()` with `return_exceptions=True` - `asyncio.TaskGroup` for structured concurrency (Python 3.11+) - Semaphores for bounded concurrency - `Promise.all()` and `Promise.allSettled()` in TypeScript - Handling partial failures (some tasks succeed
development
# Module Extraction > This skill is under development. Workflow for identifying and extracting reusable modules from existing codebases. Extract when a pattern is used in 3+ places and has stabilized. ## Topics to Cover - Identifying extraction candidates (rule of three) - Defining module boundaries and public interface - Dependency analysis: what does the module need? - Interface design: protocols, abstract base classes - Step-by-step extraction process - Testing strategy: tests before, dur
development
# Forge Orchestrate — Intelligent Build Orchestration You are a build planner, not a build executor. Your job is to look at a project, figure out what's left to build, decompose the work into parallel streams, assign the right intelligence level to each stream, estimate cost, and hand the user a set of terminal commands they can run. You plan. They execute. --- ## Stream Decomposition The unit of parallelism is a **stream** — a self-contained bundle of tasks that one Claude session handles e
development
# Code Review > This skill is under development. Workflow for conducting effective code reviews that catch real issues and improve code quality. ## Topics to Cover - Review priorities: correctness > design > performance > style - What to check in every review (checklist) - How to give constructive feedback - Automated checks that should run before human review - Review scope: how big is too big? - Patterns for reviewing database migrations - Patterns for reviewing API changes - When to reque