Home

Row Level Security

When you need granular authorization rules, nothing beats PostgreSQL's Row Level Security (RLS).

Policies are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.

Policies#

Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed. You can just think of them as adding a WHERE clause to every query. For example a policy like this ...


_10
create policy "Individuals can view their own todos."
_10
on todos for select
_10
using ( auth.uid() = user_id );

.. would translate to this whenever a user tries to select from the todos table:


_10
select *
_10
from todos
_10
where auth.uid() = todos.user_id; -- Policy is implicitly added.

Helper Functions#

Supabase provides you with a few easy functions that you can use with your policies.

auth.uid()#

Returns the ID of the user making the request.

auth.jwt()#

Returns the JWT of the user making the request.

Examples#

Here are some examples to show you the power of PostgreSQL's RLS.

Allow read access#


_15
-- 1. Create table
_15
create table profiles (
_15
id uuid references auth.users,
_15
avatar_url text
_15
);
_15
_15
-- 2. Enable RLS
_15
alter table profiles
_15
enable row level security;
_15
_15
-- 3. Create Policy
_15
create policy "Public profiles are viewable by everyone."
_15
on profiles for select using (
_15
true
_15
);

  1. Creates a table called profiles in the public schema (default schema).
  2. Enables Row Level Security.
  3. Creates a policy which allows all select queries to run.

Restrict updates#


_15
-- 1. Create table
_15
create table profiles (
_15
id uuid references auth.users,
_15
avatar_url text
_15
);
_15
_15
-- 2. Enable RLS
_15
alter table profiles
_15
enable row level security;
_15
_15
-- 3. Create Policy
_15
create policy "Users can update their own profiles."
_15
on profiles for update using (
_15
auth.uid() = id
_15
);

  1. Creates a table called profiles in the public schema (default schema).
  2. Enables RLS.
  3. Creates a policy which allows logged in users to update their own data.

Note: If you want to use upsert operations, the user needs to have INSERT, UPDATE, and SELECT permissions.

Only anon or authenticated access#

You can add a Postgres role


_10
create policy "Public profiles are viewable by everyone."
_10
on profiles for select
_10
to authenticated, anon
_10
using (
_10
true
_10
);

Policies with joins#

Policies can even include table joins. This example shows how you can query "external" tables to build more advanced rules.


_24
create table teams (
_24
id serial primary key,
_24
name text
_24
);
_24
_24
-- 2. Create many to many join
_24
create table members (
_24
team_id bigint references teams,
_24
user_id uuid references auth.users
_24
);
_24
_24
-- 3. Enable RLS
_24
alter table teams
_24
enable row level security;
_24
_24
-- 4. Create Policy
_24
create policy "Team members can update team details if they belong to the team."
_24
on teams
_24
for update using (
_24
auth.uid() in (
_24
select user_id from members
_24
where team_id = id
_24
)
_24
);

Note: If RLS is also enabled for members, the user must also have read (select) access to members. Otherwise the joined query will not yield any results.

Policies with security definer functions#

Policies can also make use of security definer functions. This is useful in a many-to-many relationship where you want to restrict access to the linking table. Following the teams and members example from above, this example shows how you can use the security definer function in combination with a policy to control access to the members table.


_27
-- 1. Follow example for 'Policies with joins' above
_27
_27
-- 2. Enable RLS
_27
alter table members
_27
enable row level security
_27
_27
-- 3. Create security definer function
_27
create or replace function get_teams_for_authenticated_user()
_27
returns setof bigint
_27
language sql
_27
security definer
_27
set search_path = public
_27
stable
_27
as $$
_27
select team_id
_27
from members
_27
where user_id = auth.uid()
_27
$$;
_27
_27
-- 4. Create Policy
_27
create policy "Team members can update team members if they belong to the team."
_27
on members
_27
for all using (
_27
team_id in (
_27
select get_teams_for_authenticated_user()
_27
)
_27
);

Verifying email domains#

Postgres has a function right(string, n) that returns the rightmost n characters of a string. You could use this to match staff member's email domains.


_16
-- 1. Create table
_16
create table leaderboard (
_16
id uuid references auth.users,
_16
high_score bigint
_16
);
_16
_16
-- 2. Enable RLS
_16
alter table leaderboard
_16
enable row level security;
_16
_16
-- 3. Create Policy
_16
create policy "Only Blizzard staff can update leaderboard"
_16
on leaderboard
_16
for update using (
_16
right(auth.jwt() ->> 'email', 13) = '@blizzard.com'
_16
);

Time to live for rows#

Policies can also be used to implement TTL or time to live feature that you see in Instagram stories or Snapchat. In the following example, rows of stories table are available only if they have been created within the last 24 hours.


_17
-- 1. Create table
_17
create table if not exists stories (
_17
id uuid not null primary key DEFAULT uuid_generate_v4(),
_17
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
_17
content text not null
_17
);
_17
_17
-- 2. Enable RLS
_17
alter table stories
_17
enable row level security;
_17
_17
-- 3. Create Policy
_17
create policy "Stories are live for a day"
_17
on stories
_17
for select using (
_17
created_at > (current_timestamp - interval '1 day')
_17
);

Advanced policies#

Use the full power of SQL to build extremely advanced rules.

In this example, we will create a posts and comments tables and then create a policy that depends on another policy. (In this case, the comments policy depends on the posts policy.)


_40
create table posts (
_40
id serial primary key,
_40
creator_id uuid not null references auth.users(id),
_40
title text not null,
_40
body text not null,
_40
publish_date date not null default now(),
_40
audience uuid[] null -- many to many table omitted for brevity
_40
);
_40
_40
create table comments (
_40
id serial primary key,
_40
post_id int not null references posts(id) on delete cascade,
_40
user_id uuid not null references auth.users(id),
_40
body text not null,
_40
comment_date date not null default now()
_40
);
_40
_40
create policy "Creator can see their own posts"
_40
on posts
_40
for select
_40
using (
_40
auth.uid() = posts.creator_id
_40
);
_40
_40
create policy "Logged in users can see the posts if they belong to the post 'audience'."
_40
on posts
_40
for select
_40
using (
_40
auth.uid() = any (posts.audience)
_40
);
_40
_40
create policy "Users can see all comments for posts they have access to."
_40
on comments
_40
for select
_40
using (
_40
exists (
_40
select 1 from posts
_40
where posts.id = comments.post_id
_40
)
_40
);

Tips#

You don't have to use policies#

You can also put your authorization rules in your middleware, similar to how you would create security rules with any other backend <-> middleware <-> frontend architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel.

Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser.

That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase.

If you plan to use this approach make sure to enable RLS for your tables. Then use the service_role key (for our client libraries) or the postgres role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient:


_10
create table profiles (
_10
id serial primary key,
_10
email text
_10
);
_10
_10
alter table profiles enable row level security;

Never use a service key on the client#

Supabase provides special "Service" keys, which can be used to bypass all RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.

caution

Initializing a client with a Service Key will not override RLS if a user token is set. If a user is signed in with a client, Supabase will adhere to the RLS policy of the user.

Testing policies#

To test policies on the database itself (i.e., from the SQL Editor or from psql) without switching to your frontend and logging in as different users, you can utilize the following helper SQL procedures (credits):


_47
grant anon, authenticated to postgres;
_47
_47
create or replace procedure auth.login_as_user (user_email text)
_47
language plpgsql
_47
as $$
_47
declare
_47
auth_user auth.users;
_47
begin
_47
select
_47
* into auth_user
_47
from
_47
auth.users
_47
where
_47
email = user_email;
_47
execute format('set request.jwt.claim.sub=%L', (auth_user).id::text);
_47
execute format('set request.jwt.claim.role=%I', (auth_user).role);
_47
execute format('set request.jwt.claim.email=%L', (auth_user).email);
_47
execute format('set request.jwt.claims=%L', json_strip_nulls(json_build_object('app_metadata', (auth_user).raw_app_meta_data))::text);
_47
_47
raise notice '%', format( 'set role %I; -- logging in as %L (%L)', (auth_user).role, (auth_user).id, (auth_user).email);
_47
execute format('set role %I', (auth_user).role);
_47
end;
_47
$$;
_47
_47
create or replace procedure auth.login_as_anon ()
_47
language plpgsql
_47
as $$
_47
begin
_47
set request.jwt.claim.sub='';
_47
set request.jwt.claim.role='';
_47
set request.jwt.claim.email='';
_47
set request.jwt.claims='';
_47
set role anon;
_47
end;
_47
$$;
_47
_47
create or replace procedure auth.logout ()
_47
language plpgsql
_47
as $$
_47
begin
_47
set request.jwt.claim.sub='';
_47
set request.jwt.claim.role='';
_47
set request.jwt.claim.email='';
_47
set request.jwt.claims='';
_47
set role postgres;
_47
end;
_47
$$;

To switch to a given user (by email), use call auth.login_as_user('my@email.com');. You can also switch to the anon role using call auth.login_as_anon();. When you are done, use call auth.logout(); to return yourself to the postgres role.

These procedures can also be used for writing pgTAP unit tests for policies.

Click here to see an example psql interaction using this.

This example shows that the public.profiles table from the tutorial example can indeed be updated by the postgres role and the owner of the row but not from anon connections:


_54
postgres=> select id, email from auth.users;
_54
id | email
_54
--------------------------------------+-------------------
_54
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | user1@example.com
_54
15d6811a-16ee-4fa2-9b18-b63085688be4 | user2@example.com
_54
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | user3@example.com
_54
(3 rows)
_54
_54
postgres=> table public.profiles;
_54
id | updated_at | username | full_name | avatar_url | website
_54
--------------------------------------+------------+----------+-----------+------------+---------
_54
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
_54
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
_54
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
_54
(3 rows)
_54
_54
postgres=> call auth.login_as_anon();
_54
CALL
_54
postgres=> update public.profiles set updated_at=now();
_54
UPDATE 0 -- anon users cannot update any profile but see all of them
_54
postgres=> table public.profiles;
_54
id | updated_at | username | full_name | avatar_url | website
_54
--------------------------------------+------------+----------+-----------+------------+---------
_54
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
_54
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
_54
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
_54
(3 rows)
_54
_54
postgres=> call auth.logout();
_54
CALL
_54
postgres=> call auth.login_as_user('user1@example.com');
_54
NOTICE: set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com')
_54
CALL
_54
postgres=> update public.profiles set updated_at=now();
_54
UPDATE 1 -- authenticated users can update their own profile and see all of them
_54
postgres=> table public.profiles;
_54
id | updated_at | username | full_name | avatar_url | website
_54
--------------------------------------+-------------------------------+----------+-----------+------------+---------
_54
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user1 | User 1 | |
_54
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user2 | User 2 | |
_54
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:39:16.204612+00 | user3 | User 3 | |
_54
(3 rows)
_54
_54
postgres=> call auth.logout();
_54
CALL
_54
postgres=> update public.profiles set updated_at=now();
_54
UPDATE 3 -- the 'postgres' role can update and see all profiles
_54
postgres=> table public.profiles;
_54
id | updated_at | username | full_name | avatar_url | website
_54
--------------------------------------+-------------------------------+----------+-----------+------------+---------
_54
15d6811a-16ee-4fa2-9b18-b63085688be4 | 2023-02-18 21:40:08.216324+00 | user1 | User 1 | |
_54
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | 2023-02-18 21:40:08.216324+00 | user2 | User 2 | |
_54
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:40:08.216324+00 | user3 | User 3 | |
_54
(3 rows)

Deprecated features#

We have deprecate some functions to ensure better performance and extensibilty of RLS policies.

auth.role()#

caution

Deprecated: The auth.role() function has been deprecated in favour of using the TO field, natively supported within Postgres.


_13
-- DEPRECATED
_13
create policy "Public profiles are viewable by everyone."
_13
on profiles for select using (
_13
auth.role() = 'authenticated' or auth.role() = 'anon'
_13
);
_13
_13
-- RECOMMENDED
_13
create policy "Public profiles are viewable by everyone."
_13
on profiles for select
_13
to authenticated, anon
_13
using (
_13
true
_13
);

auth.email()#

caution

Deprecated. Use auth.jwt() ->> 'email' instead.

Returns the email of the user making the request.