Esta página está disponível apenas em inglês.
Supabase + RLS
Pathrule3 regras • 3 memórias • 1 skill
Rules, memories, and a review skill for Supabase Postgres projects that take Row Level Security seriously. Pre-scoped to your supabase directory so your AI assistant enforces RLS, keeps the service role out of clients, and follows a disciplined migration flow. It also covers the migration workflow, so policies are versioned with the schema instead of being clicked into the dashboard and lost.
Mapa de caminhos sugerido
O Pathrule coloca cada item no caminho correspondente, para que seu assistente só o veja onde ele pertence. Este é o escopo que você recebe ao importar; você pode ajustá-lo no seu espaço de trabalho.
Regras
3RLS enabled on every table, deny by default/supabasealtaestritaNo table ships without Row Level Security enabled and explicit per-operation policies.
| 1 | Every user-facing table in the public schema must have Row Level Security enabled with a deny-by-default posture. A table with RLS disabled is readable and writable by any authenticated user. |
| 2 | |
| 3 | - Enable RLS in the same migration that creates the table: `ALTER TABLE <name> ENABLE ROW LEVEL SECURITY`. Never rely on the application layer to enforce access at the row level. |
| 4 | - Start from no policies (which denies everything) and add the minimum set of policies per operation (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) that the feature actually requires. A catch-all `USING (true)` policy is equivalent to no RLS. |
| 5 | - A migration that creates a table without enabling RLS and adding policies is incomplete and must not merge to a shared environment. |
| 6 | - Internal or log tables used only by server-side jobs that run as the service role may skip per-user policies, but must still have RLS enabled and an explicit `USING` expression that allows only the service role, not a blanket bypass. |
Service role key stays server-side; never in client paths/supabasealtaestritaThe service role key bypasses RLS entirely and must never appear in browser code, client environment variables, or per-request edge function logic acting on behalf of a user.
| 1 | The Supabase service role key grants full unrestricted access to every row in every table, bypassing all RLS policies. Leaking it to a browser or a client-accessible path renders all RLS useless. |
| 2 | |
| 3 | - Never include the service role key in any environment variable exposed to the browser (for example `NEXT_PUBLIC_*` in Next.js, or any variable bundled by Vite/webpack for the client). |
| 4 | - Edge functions that handle per-user requests must create their Supabase client with the user's JWT from the `Authorization` header, not the service role: `createClient(url, anonKey, { global: { headers: { Authorization: req.headers.get('Authorization') } } })`. This ensures RLS evaluates under the correct user identity. |
| 5 | - Use the service role key only in trusted server-side contexts (scheduled functions, admin scripts, server-to-server calls) where bypassing RLS is the deliberate intent and the scope is understood. |
| 6 | - Never derive or reconstruct the service role key from a client request, environment variable pattern, or any value accessible to end users. |
Index every column used in RLS policy predicates/supabase/migrationsmédiaconsultivaAn unindexed column in a USING or WITH CHECK expression causes a full-table scan on every row-level check.
| 1 | Postgres evaluates each RLS policy's `USING` and `WITH CHECK` expressions once per row that the query touches. An expression that references an unindexed column forces a sequential scan on every row access, not just on the query plan, and the cost is invisible in `EXPLAIN` for the outer query but shows up as degraded performance under load. |
| 2 | |
| 3 | - Add a B-tree index on every column that appears in a `USING (...)` or `WITH CHECK (...)` expression, particularly `user_id`, `workspace_id`, `tenant_id`, and any other access-boundary column. |
| 4 | - For policies that call `auth.uid()`, the `user_id` column they compare against must be indexed; the `auth.uid()` itself is a function call that Postgres inlines as a constant per statement, so it is not the bottleneck, but the column scan is. |
| 5 | - If using a `SECURITY DEFINER` helper function (see the access-helper memory), index the columns inside that function's WHERE clause, not just the surface-level expression in the policy. |
| 6 | - Run `EXPLAIN ANALYZE` on representative queries before and after adding RLS to confirm the policy predicates are not adding full-table sequential scans. |
Memórias
3Centralize access checks in a SECURITY DEFINER helper/supabaseOne SQL function that expresses membership or ownership, reused by all policies across tables.
| 1 | Writing the same `EXISTS (SELECT 1 FROM memberships WHERE ...)` check inline in multiple policies creates drift: tables diverge, a bug fix in one policy misses others, and refactoring access logic means touching every policy. |
| 2 | |
| 3 | - Express membership and ownership in one `SECURITY DEFINER` function, for example: |
| 4 | ```sql |
| 5 | CREATE OR REPLACE FUNCTION has_workspace_access(p_workspace_id uuid) |
| 6 | RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $$ |
| 7 | SELECT EXISTS ( |
| 8 | SELECT 1 FROM workspace_members |
| 9 | WHERE workspace_id = p_workspace_id |
| 10 | AND user_id = auth.uid() |
| 11 | ); |
| 12 | $$; |
| 13 | ``` |
| 14 | - Reference the helper in all relevant policies: `CREATE POLICY "workspace read" ON documents FOR SELECT USING (has_workspace_access(workspace_id))`. |
| 15 | - `SECURITY DEFINER` runs the function as the function owner (typically `postgres`), which avoids RLS recursion if `workspace_members` itself is RLS-protected. Mark it `STABLE` so the planner can cache the result per statement. |
| 16 | - Grant `EXECUTE` only to `authenticated` and `service_role`; do not grant to `anon` unless anonymous access is intentional. |
auth.uid(), auth.jwt(), and custom claims in policies/supabaseHow to use Supabase JWT claims in RLS expressions and how to attach custom claims via a hook without performance footguns.
| 1 | Supabase injects the authenticated user's identity into the Postgres session so RLS policies can reference it without a subquery to `auth.users`. |
| 2 | |
| 3 | - `auth.uid()` returns the `sub` claim of the JWT as a `uuid`. Use it directly in policy `USING` expressions: `USING (user_id = auth.uid())`. |
| 4 | - `auth.jwt()` returns the full decoded JWT as `jsonb`. Access top-level claims with `->>`: `auth.jwt() ->> 'role'`. For nested claims (for example `app_metadata`): `(auth.jwt() -> 'app_metadata') ->> 'plan'`. |
| 5 | - Custom claims (roles, plan tier, org ID) are the recommended way to encode authorization context without an extra DB lookup per row. Add them via a Supabase Auth Hook (the `custom_access_token` hook in the Dashboard), which runs a Postgres function that appends claims to every JWT before it is issued. |
| 6 | - Never call `SELECT ... FROM auth.users` inside a policy `USING` expression just to read a claim that is already in the JWT. It issues a subquery per row. Use `auth.jwt()` to read the claim from the already-present token instead. |
| 7 | - When a custom claim must be refreshed (for example after a plan upgrade), call `supabase.auth.refreshSession()` on the client to force a new token with updated claims; old tokens keep the stale claims until they expire. |
Supabase migration workflow/supabase/migrationsForward-only timestamped SQL, RLS and policies in the same migration as the table, TypeScript types regenerated after each change.
| 1 | Supabase migrations are plain SQL files managed by the Supabase CLI. The workflow keeps the database and generated types in sync. |
| 2 | |
| 3 | - Generate a new migration with `supabase migration new <description>`, which creates a timestamped file in `supabase/migrations/`. Write forward-only SQL; there are no down steps. |
| 4 | - Enable RLS and add all required policies in the same migration file that creates the table. Never leave the table in a state where it exists but RLS is not yet enabled, even briefly across separate migrations. |
| 5 | - After applying a migration that changes the schema, regenerate TypeScript types: `supabase gen types typescript --linked > src/types/supabase.ts` (or equivalent). Commit the updated types file in the same PR as the migration. |
| 6 | - Apply locally with `supabase db reset` (for local dev) or `supabase db push` (for remote preview branches). Never edit a migration file that has already been applied to any shared environment; add a new migration instead. |
| 7 | - For Supabase hosted projects, use branching (`supabase branches`) to develop and test schema changes on an isolated preview branch before merging to production. |
Skills
1supabase-rls-review/rootChecklist for reviewing a Supabase schema or migration change for RLS correctness, performance, and workflow safety.
| 1 | --- |
| 2 | name: supabase-rls-review |
| 3 | description: Review a Supabase migration or schema change for RLS correctness, JWT claim usage, access-helper design, policy-predicate indexing, and migration workflow safety. |
| 4 | --- |
| 5 | |
| 6 | # Supabase RLS review |
| 7 | |
| 8 | ## RLS and policies |
| 9 | - [ ] RLS is enabled on every new table in the same migration that creates it. |
| 10 | - [ ] Policies exist per operation (`SELECT`, `INSERT`, `UPDATE`, `DELETE`) and the default is deny (no `USING (true)` catch-all). |
| 11 | - [ ] Policies use the shared `SECURITY DEFINER` access helper, not ad-hoc inline subqueries that would differ across tables. |
| 12 | - [ ] Internal/job tables accessed only via the service role have explicit `USING` expressions limiting to the service role, not a blanket bypass. |
| 13 | |
| 14 | ## Performance |
| 15 | - [ ] Every column referenced in a `USING` or `WITH CHECK` expression (especially `user_id`, `workspace_id`, `tenant_id`) has a B-tree index. |
| 16 | - [ ] No policy calls `SELECT ... FROM auth.users` per row to read a claim that is already available via `auth.jwt()`. |
| 17 | |
| 18 | ## JWT and custom claims |
| 19 | - [ ] Policies use `auth.uid()` for user identity and `auth.jwt()` for custom claims; no unnecessary subqueries to `auth.users`. |
| 20 | - [ ] Custom claims added via the Auth Hook are documented and the client calls `refreshSession()` after claim-changing events (plan upgrade, role change). |
| 21 | |
| 22 | ## Service role |
| 23 | - [ ] No service role key appears in any browser-accessible environment variable or in edge function code acting on behalf of a user. |
| 24 | - [ ] Edge functions acting on behalf of a user forward the `Authorization: Bearer <user_jwt>` header to the Supabase client, not the service role key. |
| 25 | |
| 26 | ## Migration workflow |
| 27 | - [ ] TypeScript types are regenerated and committed alongside the migration. |
| 28 | - [ ] Migration is forward-only and no already-applied file was edited; a new file was added. |
Por que este padrão
Postgres tables ship without Row Level Security, or the service role key sneaks into client paths, leaving data open to anyone.
Feito para: teams building on Supabase Postgres with multi-tenant or per-user data.
Evita que seu assistente:
- Creating a table without RLS enabled and policies
- Using the service role key where the user's JWT belongs
- Ad hoc access checks that drift apart across tables
- Licença
- Apache-2.0
- Versão
- 1.0.1
- Atualizado em
- 2026-08-25