I was recently helping a Clerk user get their user data integrated with a Supabase database.
This process turned me onto Row Level Security, which is a Postgres feature that the integration leverages. It was my first time walking through Clerk’s integration with Supabase, so I had to dig deep into RLS to understand what it is and how it works. Instead of syncronizing user data with a specific table as I was expecting, the integration instead utilizes the Clerk user ID to prevent access to data in any table WITHOUT modifying any SQL queries.
In this article, I walk through what Row Level Security is and how to use it in your Postgres or Supabase database.
What is Postgres Row Level Security?
Row Level Security (RLS) is a way to restrict what a given user can read and write in a Postgres database. This is accomplished through policies. Policies created for select
and delete
statements effectively append to the where
clause of the query to ensure the correct data is returned. Policies on statements that modify data like insert
or update
will check the query to make sure no violations occur, preventing data from being modified when it shouldn’t be.
How to enable row level security
RLS is enabled per table using the following query:
alter table orders enable row level security;
Once enabled, policies can be created on the table to apply the checks and filters. For instance, the following policy will restrict what can be selected from the orders
table by filtering out any records where user_id
does not match the currently logged in user:
create policy "select_by_user_id" on orders
for select using (current_user() = user_id);
This means that when a select * from orders;
is run, the policy will be evaluated and the resulting query is closer to what be executed instead:
select * from orders where id = current_user();
The syntax for policies created on data modification queries is similar but the using
keyword is replaced with with check
like so:
create policy "insert_by_user_id" on orders
for insert with check (current_user() = user_id);
Policy types
You might come across examples with the as permissive
or as restrictive
keywords added. When applied, these change the way multiple policies are evaluated on a query.
Permissive policies are joined with an or
operator, whereas restrictive policies are joined with an and
operator. By default, all policies are permissive.
Given the following policies:
create policy "select_by_user_id" on orders
for select using (current_user() = user_id);
create policy "select_recent_orders" on orders
for select using (created > NOW() - INTERVAL '7 days');
Running a select *
query evaluates to the following:
select * from orders
where user_id = current_user()
or created > NOW() - INTERVAL '7 days';
These same policies can be set as restrictive:
create policy "select_by_user_id" on orders
for select as restrictive using (current_user() = user_id);
create policy "select_recent_orders" on orders
for select as restrictive using (created > NOW() - INTERVAL '7 days');
And the resulting query would swap the or
with an and
:
select * from orders
where user_id = current_user()
and created > NOW() - INTERVAL '7 days';
In this example, the desired effect is probably restrictive
, but thats entirely up to you as the designer of your database.