Back to blogs

Supabase Row Level Security Explained With Real Examples

September 12, 2025
6 min read
Supabase Row Level Security Explained With Real Examples

When I first started using Supabase, one of the most confusing (but also powerful) features I came across was Row-Level Security (RLS). At first, it felt a bit technical, but once I understood it properly, I realised how important it is for protecting data. In this blog, I’ll give an overview of what RLS is, why it matters, and show real examples of how I use it in projects.


What is Row-Level Security (RLS)?

In simple words, RLS controls which rows of a table a user can access. Instead of handling all permission checks in your frontend or backend code, you define rules directly inside the database.

For example, if you have a todos table where each row belongs to a specific user, RLS can make sure users only see their own todos — not someone else’s.

Supabase uses PostgreSQL’s RLS feature under the hood, so you get the same reliability and security but with an easier developer experience.


Why RLS is important (my perspective)

I see RLS as a safety net for my apps. Here’s why:

  1. Centralised rules – I don’t need to repeat permission checks in multiple parts of my code.
  2. Stronger security – Even if someone finds a bug in my API, the database itself won’t return data they shouldn’t see.
  3. Easier scaling – If I add more features or clients later, the same RLS policies still apply.

Without RLS, you risk exposing sensitive data by mistake. With RLS, the database itself enforces who sees what.


How RLS works in Supabase

There are three main things to understand:

  1. Enable RLS on a table – By default, it’s off. You need to enable it to apply policies.
  2. Policies – These are rules you define, like “users can only select rows where user_id = auth.uid().”
  3. Roles – Supabase has built-in roles like anon (not logged in) and authenticated (logged in). You can target policies for each role.

Once you enable RLS and add policies, Supabase automatically enforces them on all queries.


Real Examples of RLS

Here are three examples that I find most useful in real projects.


Example 1: Per-user data (Todos app)

ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can view their own todos"
ON todos
FOR SELECT
TO authenticated
USING (auth.uid() = user_id);

CREATE POLICY "Users can insert their own todos"
ON todos
FOR INSERT
TO authenticated
WITH CHECK (auth.uid() = user_id);

Here, each logged-in user can only see and add their own todos. Even if they try to query all todos, the database will only return rows matching their user_id.


Example 2: Admins vs normal users

Let’s say you have a projects table:

  1. Admins can see all projects.
  2. Regular users can only see projects they own or are members of.


ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Admins can see all projects"
ON projects
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_roles.user_id = auth.uid()
AND user_roles.role = 'admin'
)
);

CREATE POLICY "Users see only their projects"
ON projects
FOR SELECT
TO authenticated
USING (
auth.uid() = owner_id
OR EXISTS (
SELECT 1 FROM project_members
WHERE project_members.project_id = projects.id
AND project_members.user_id = auth.uid()
)
);

This way, admins have full access, while regular users are restricted to their own projects.


Example 3: Multi-tenant SaaS (Company separation)

In SaaS apps, you often need to keep each company’s data separate. You can add a tenant_id column and restrict rows by tenant.


ALTER TABLE tenant_data ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Tenants can only access their own data"
ON tenant_data
FOR ALL
TO authenticated
USING (
tenant_id = (
SELECT tenant_id FROM users WHERE users.id = auth.uid()
)
)
WITH CHECK (
tenant_id = (
SELECT tenant_id FROM users WHERE users.id = auth.uid()
)
);


Now, even if two companies are on the same database, they’ll never see each other’s data.


Common mistakes I’ve seen (and made myself)

When I was learning RLS, I ran into these issues:

  1. Forgetting to enable RLS – Policies won’t work until you explicitly enable it.
  2. Too broad policies – Writing USING (true) means every logged-in user can access all rows (bad idea).
  3. Performance problems – If policies use complex joins without indexes, queries get slow. Always index columns like user_id or tenant_id.
  4. Assuming RLS replaces all backend logic – RLS is strong, but sometimes you still need app-level checks for complex workflows.


Best practices that help me

  1. Enable RLS from day one – Don’t leave it for later.
  2. Keep policies simple – Start with basic checks, then refine.
  3. Use custom JWT claims – Store roles or tenant IDs in tokens so policies don’t need heavy subqueries.
  4. Add indexes – Any column used in policies should be indexed.
  5. Test with different users – Supabase dashboard lets you test queries as specific users.


What’s new in 2025

A few updates I’ve noticed in the Supabase community:

  1. More developers are using JWT claims to simplify policies and improve performance.
  2. Supabase added detailed docs on RLS performance best practices (like indexing and keeping expressions simple).
  3. Developers are mixing RLS with backend APIs for complex apps, instead of relying only on database rules.


Final thoughts

For me, Supabase RLS is one of those features that feels tricky at first but pays off massively once you get the hang of it. It keeps your data safe, simplifies your code, and makes scaling easier.

If you’re starting a new project, I’d suggest:

  1. Enable RLS early.
  2. Keep policies clear and test them often.
  3. Use indexes and JWT claims for performance.


Done right, RLS acts like a solid security guard for your database — always on duty, making sure no one sees what they shouldn’t.

supabase row level securityrls in supabasesupabase rls examplesrow level security postgresqlsupabase database securitysupabase rls tutorialhow to enable rls in supabasesupabase rls policiessupabase authentication and rlsmulti tenant app supabase rlssupabase rls best practicessupabase rls vs backend checksrow level security for saas appssupabase rls explainedsupabase rls step by step guidesupabase security tips

Recent Blogs

View All