
Supabase Security
- 47 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with security tasks during AI-assisted development.
About
supabase-security is a Claude Code skill for security. It helps solo builders move faster with AI-assisted coding.
- supabase-security
- Security
- AI-coding skill
Supabase Security by the numbers
- 47 all-time installs (skills.sh)
- Ranked #1,355 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill supabase-securityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with security tasks during AI-assisted development.
Files
Supabase Security
Identity
You are a Supabase security expert. RLS is mandatory on every table. Service role key is nuclear - server only. Trust only auth.uid().
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Supabase Security
Patterns
---
Name
RLS Policy Types
Description
All four policy types
When
Setting up table security
Example
-- SELECT policy create policy "Users see own" on profiles for select using (auth.uid() = user_id);
-- INSERT policy create policy "Users create own" on profiles for insert with check (auth.uid() = user_id);
-- UPDATE policy (needs both) create policy "Users update own" on profiles for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
-- DELETE policy create policy "Users delete own" on profiles for delete using (auth.uid() = user_id);
---
Name
Role-Based Access Control
Description
RBAC using JWT claims
When
Different permissions per role
Example
create or replace function is_admin() returns boolean as $$ select coalesce( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin', false ); $$ language sql security definer;
create policy "Admin access" on admin_data for all using (is_admin());
---
Name
Multi-Tenant RLS
Description
Isolate data between orgs
When
SaaS with multiple organizations
Example
create or replace function user_org_ids() returns setof uuid as $$ select org_id from org_members where user_id = auth.uid() $$ language sql security definer stable;
create policy "Org access" on projects for select using (org_id in (select user_org_ids()));
-- CRITICAL: Index for performance create index idx_org_members_user on org_members(user_id);
---
Name
Storage Security
Description
Secure file uploads
When
Private file storage
Example
-- Private bucket insert into storage.buckets (id, name, public) values ('user-files', 'user-files', false);
-- User folder policy: user-files/{user_id}/file.ext create policy "Own files" on storage.objects for select using ( bucket_id = 'user-files' and auth.uid()::text = (storage.foldername(name))[1] );
---
Name
Service Role vs Anon Key
Description
When to use each
When
Choosing auth method
Example
// ANON KEY - safe for client, subject to RLS const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY );
// SERVICE ROLE - BYPASSES RLS, server only\! const supabaseAdmin = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY // NO NEXT_PUBLIC_\! );
---
Name
Edge Function Auth
Description
Verify tokens in Edge Functions
When
Serverless functions
Example
const authHeader = req.headers.get("Authorization"); const supabase = createClient(url, anonKey, { global: { headers: { Authorization: authHeader } } }); const { data: { user } } = await supabase.auth.getUser(); if (\!user) return new Response("Unauthorized", { status: 401 });
---
Name
Preventing IDOR
Description
Insecure Direct Object Reference
When
APIs taking IDs from client
Example
-- Without RLS, client can query any user: -- .from("profiles").eq("id", anyUserId)
-- With RLS policy, safe: create policy "Own profile" on profiles for select using (auth.uid() = id);
---
Name
Preventing Privilege Escalation
Description
Stop role self-elevation
When
User profiles with roles
Example
create policy "Update own profile" on users for update using (auth.uid() = id) with check ( role = (select role from users where id = auth.uid()) ); -- Role must stay unchanged
Anti-Patterns
---
Name
RLS Disabled
Description
Tables without RLS
Why
Anyone can read/write all data
Instead
Enable RLS on every table
---
Name
Service Role in Client
Description
Service key in frontend
Why
Full database access exposed
Instead
Server only, no NEXT_PUBLIC_
---
Name
Trusting Client ID
Description
Client-provided user_id
Why
Users can impersonate others
Instead
Always use auth.uid()
---
Name
Complex Policy Logic
Description
Business logic in policies
Why
Kills performance
Instead
Use security definer functions
---
Name
Missing Policy Indexes
Description
RLS on non-indexed columns
Why
Full table scan
Instead
Index policy columns
Supabase Security - Sharp Edges
Rls Disabled Table
Id
rls-disabled-table
Summary
Table with RLS disabled exposes all data
Severity
critical
Situation
You create a table and forget to enable RLS. Or you disable it for testing and forget to re-enable. Anyone with the anon key can now read and write all data in that table.
Why
By default, tables are accessible to anyone. RLS is opt-in. The anon key is public (in your frontend bundle). Without RLS, that key grants full access to the table.
Solution
-- Enable RLS on EVERY table alter table users enable row level security; alter table posts enable row level security; alter table comments enable row level security;
-- Even for public data, enable RLS with permissive policy alter table public_posts enable row level security; create policy "Anyone can read" on public_posts for select using (true);
Symptoms
- Data visible to unauthenticated users
- Users can see other users data
- Data modified without authorization
Service Role Exposed
Id
service-role-exposed
Summary
Service role key in client-side code
Severity
critical
Situation
You need to bypass RLS for an admin feature. You use the service role key. You accidentally put it in an environment variable with NEXT_PUBLIC_ prefix, or import it in client code.
Why
Service role key bypasses ALL security. Anyone can extract it from your frontend bundle. They now have full database access - read all data, delete everything, impersonate any user.
Solution
// WRONG - exposed to client const supabase = createClient(url, process.env.NEXT_PUBLIC_SERVICE_KEY);
// RIGHT - server only // In server action or API route: const supabaseAdmin = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY // No NEXT_PUBLIC_ );
// Check your .env files: // .env.local should have: // SUPABASE_SERVICE_ROLE_KEY=... (no NEXT_PUBLIC_)
Symptoms
- Service key visible in browser devtools
- Unauthorized data access
- Data deletion or corruption
Policy Missing Auth Check
Id
policy-missing-auth-check
Summary
RLS policy without auth.uid() check
Severity
critical
Situation
You write a policy that checks a column value but forgets to verify the user owns the row. Users can manipulate the query to access other users data.
Why
RLS policies must anchor to auth.uid(). If you only check "status = published", any user can see any published row, even private ones with status published.
Solution
-- WRONG: No ownership check create policy "See published" on posts for select using (status = 'published'); -- Anyone sees all published\!
-- RIGHT: Include ownership create policy "See own or published" on posts for select using ( auth.uid() = author_id OR status = 'published' );
Symptoms
- Users see data they should not
- Private data leaks
Insert Without User Id
Id
insert-without-user-id
Summary
INSERT policy allows any user_id value
Severity
high
Situation
Your INSERT policy uses with check (true) or forgets to verify user_id matches auth.uid(). Users can insert records as other users.
Why
INSERT policies need with check, not using. If you do not validate user_id, users can set any value and impersonate others.
Solution
-- WRONG: No user_id validation create policy "Insert posts" on posts for insert with check (true); -- Anyone can set any user_id\!
-- RIGHT: Force user_id to match create policy "Insert own posts" on posts for insert with check (auth.uid() = user_id);
-- Also validate other fields create policy "Insert safe" on posts for insert with check ( auth.uid() = user_id AND status in ('draft', 'published') );
Symptoms
- Records created with wrong user_id
- Impersonation attacks
Update Missing With Check
Id
update-missing-with-check
Summary
UPDATE policy without with check allows privilege escalation
Severity
high
Situation
Your UPDATE policy only has using() but no with check(). Users can modify their rows to values they should not have - like setting role to admin.
Why
using() controls which rows are visible. with check() controls what values are allowed. Without with check, any value can be written.
Solution
-- WRONG: No value validation create policy "Update own" on users for update using (auth.uid() = id); -- Can set role = 'admin'\!
-- RIGHT: Validate new values create policy "Update own safe" on users for update using (auth.uid() = id) with check ( auth.uid() = id AND role = (select role from users where id = auth.uid()) );
Symptoms
- Users elevate their own privileges
- Protected fields modified
Storage Public Bucket
Id
storage-public-bucket
Summary
Storage bucket set to public unintentionally
Severity
high
Situation
You create a bucket for user uploads and set public = true to make URLs work. Now anyone can list and download all files without auth.
Why
Public buckets expose all files via predictable URLs. Even without listing, files can be accessed if the path is guessed or leaked.
Solution
-- Create PRIVATE bucket insert into storage.buckets (id, name, public) values ('uploads', 'uploads', false); -- public = false\!
-- Add RLS policy for access create policy "Users access own files" on storage.objects for select using ( bucket_id = 'uploads' and auth.uid()::text = (storage.foldername(name))[1] );
-- For downloads, use signed URLs from server
Symptoms
- Files accessible without login
- Private files exposed publicly
Policy Performance
Id
policy-performance
Summary
Complex RLS policy causes slow queries
Severity
medium
Situation
Your policy does a subquery or function call. Every query now takes seconds instead of milliseconds. The table grows and it gets worse.
Why
RLS policies run on every row. Complex logic means complex execution on every query. No index can help if the policy itself is slow.
Solution
-- WRONG: Subquery in policy create policy "Team access" on docs for select using ( team_id in (select team_id from team_members where user_id = auth.uid()) ); -- Runs subquery for EVERY row\!
-- RIGHT: Use security definer function create or replace function user_team_ids() returns setof uuid as 96384 select team_id from team_members where user_id = auth.uid() 96384 language sql security definer stable;
-- Cache result per query create policy "Team access" on docs for select using (team_id in (select user_team_ids()));
-- Add index create index idx_team_members_user on team_members(user_id);
Symptoms
- Queries take seconds
- Performance degrades with data growth
Supabase Security - Validations
Service role key in client code
Id
service-role-exposed
Severity
critical
Type
regex
Pattern
- NEXT_PUBLIC.SERVICE.ROLE
- NEXT_PUBLIC.service.role
- process\.env\.NEXT_PUBLIC.*SERVICE
Message
Service role key must never be exposed to client
Fix Action
Remove NEXT_PUBLIC_ prefix, use only in server code
Applies To
- *.ts
- *.tsx
- *.js
- .env*
RLS explicitly disabled
Id
rls-disabled
Severity
critical
Type
regex
Pattern
- disable row level security
- ALTER TABLE.DISABLE.RLS
Message
RLS should never be disabled on production tables
Fix Action
Enable RLS and create appropriate policies
Applies To
- *.sql
Overly permissive RLS policy
Id
permissive-policy
Severity
high
Type
regex
Pattern
- using\s\(\strue\s*\)
- with check\s\(\strue\s*\)
Message
Policies with (true) allow all access
Fix Action
Add proper auth.uid() checks
Applies To
- *.sql
Policy without auth.uid() check
Id
missing-auth-uid
Severity
warning
Type
regex
Pattern
- create policy(?!.auth\.uid).using
Message
Consider adding auth.uid() check to policy
Fix Action
Verify policy properly restricts access
Applies To
- *.sql
Public storage bucket
Id
public-bucket
Severity
warning
Type
regex
Pattern
- public.=.true
- public:\s*true
Message
Public buckets expose all files
Fix Action
Use private bucket with RLS policies
Applies To
- *.sql
Hardcoded Supabase keys
Id
anon-key-hardcoded
Severity
warning
Type
regex
Pattern
- eyJ[A-Za-z0-9_-]{50,}
Message
Do not hardcode Supabase keys
Fix Action
Use environment variables
Applies To
- *.ts
- *.tsx
- *.js