Row-Level Security is one of those Postgres features that feels intimidating until you have a few patterns memorized. After shipping ~6 apps on Supabase, here are the ones I copy-paste constantly.
1. Owner-only
The classic: a user can only see or modify their own rows.
CREATE POLICY "Owner read" ON tasks
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Owner write" ON tasks
FOR ALL USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);The WITH CHECK is critical for inserts/updates — without it, a user could insert a row with someone else's user_id.
2. Team-scoped
Users see anything in a team they're a member of.
CREATE POLICY "Team read" ON projects
FOR SELECT USING (
team_id IN (
SELECT team_id FROM team_members WHERE user_id = auth.uid()
)
);3. Soft-delete
Hide rows where deleted_at is set, except to admins.
CREATE POLICY "Hide soft-deleted" ON posts
FOR SELECT USING (
deleted_at IS NULL
OR auth.jwt() ->> 'role' = 'admin'
);4. Public read, owner write
For things like blog posts or project showcases.
CREATE POLICY "Anyone can read" ON posts
FOR SELECT USING (true);
CREATE POLICY "Only owner can write" ON posts
FOR INSERT WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Only owner can update" ON posts
FOR UPDATE USING (auth.uid() = author_id);5. Admin override
A "skip RLS" door for service code, without disabling RLS globally.
CREATE POLICY "Admin override" ON anything
FOR ALL USING (auth.jwt() ->> 'role' = 'admin');Then call this from your server with the service role key — your client code stays locked down.
The one rule I follow
Never disable RLS on a production table. If a query needs to bypass it, that query should be running with the service role key from a server, not the anon key from a client. That single rule has saved me from a whole class of bugs.