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 ...
_10create 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:
_10select *_10from todos_10where 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_15create table profiles (_15 id uuid references auth.users,_15 avatar_url text_15);_15_15-- 2. Enable RLS_15alter table profiles_15 enable row level security;_15_15-- 3. Create Policy_15create policy "Public profiles are viewable by everyone."_15 on profiles for select using (_15 true_15 );
- Creates a table called
profiles
in the public schema (default schema). - Enables Row Level Security.
- Creates a policy which allows all
select
queries to run.
Restrict updates#
_15-- 1. Create table_15create table profiles (_15 id uuid references auth.users,_15 avatar_url text_15);_15_15-- 2. Enable RLS_15alter table profiles_15 enable row level security;_15_15-- 3. Create Policy_15create policy "Users can update their own profiles."_15 on profiles for update using (_15 auth.uid() = id_15 );
- Creates a table called
profiles
in the public schema (default schema). - Enables RLS.
- 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
_10create policy "Public profiles are viewable by everyone."_10on profiles for select_10to authenticated, anon_10using (_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.
_24create table teams (_24 id serial primary key,_24 name text_24);_24_24-- 2. Create many to many join_24create table members (_24 team_id bigint references teams,_24 user_id uuid references auth.users_24);_24_24-- 3. Enable RLS_24alter table teams_24 enable row level security;_24_24-- 4. Create Policy_24create 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_27alter table members_27 enable row level security_27_27-- 3. Create security definer function_27create or replace function get_teams_for_authenticated_user()_27returns setof bigint_27language sql_27security definer_27set search_path = public_27stable_27as $$_27 select team_id_27 from members_27 where user_id = auth.uid()_27$$;_27_27-- 4. Create Policy_27create 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_16create table leaderboard (_16 id uuid references auth.users,_16 high_score bigint_16);_16_16-- 2. Enable RLS_16alter table leaderboard_16 enable row level security;_16_16-- 3. Create Policy_16create 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_17create 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_17alter table stories_17 enable row level security;_17_17-- 3. Create Policy_17create 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.)
_40create 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_40create 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_40create policy "Creator can see their own posts"_40on posts_40for select_40using (_40 auth.uid() = posts.creator_id_40);_40_40create policy "Logged in users can see the posts if they belong to the post 'audience'."_40on posts_40for select_40using (_40 auth.uid() = any (posts.audience)_40);_40_40create policy "Users can see all comments for posts they have access to."_40on comments_40for select_40using (_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:
_10create table profiles (_10 id serial primary key,_10 email text_10);_10_10alter 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):
_47grant anon, authenticated to postgres;_47_47create or replace procedure auth.login_as_user (user_email text)_47 language plpgsql_47 as $$_47declare_47 auth_user auth.users;_47begin_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);_47end;_47$$;_47_47create or replace procedure auth.login_as_anon ()_47 language plpgsql_47 as $$_47begin_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;_47end;_47$$;_47_47create or replace procedure auth.logout ()_47 language plpgsql_47 as $$_47begin_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;_47end;_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:
_54postgres=> 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_54postgres=> 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_54postgres=> call auth.login_as_anon();_54CALL_54postgres=> update public.profiles set updated_at=now();_54UPDATE 0 -- anon users cannot update any profile but see all of them_54postgres=> 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_54postgres=> call auth.logout();_54CALL_54postgres=> call auth.login_as_user('user1@example.com');_54NOTICE: set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com')_54CALL_54postgres=> update public.profiles set updated_at=now();_54UPDATE 1 -- authenticated users can update their own profile and see all of them_54postgres=> 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_54postgres=> call auth.logout();_54CALL_54postgres=> update public.profiles set updated_at=now();_54UPDATE 3 -- the 'postgres' role can update and see all profiles_54postgres=> 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_13create policy "Public profiles are viewable by everyone."_13on profiles for select using (_13 auth.role() = 'authenticated' or auth.role() = 'anon'_13);_13_13-- RECOMMENDED_13create policy "Public profiles are viewable by everyone."_13on profiles for select_13to authenticated, anon_13using (_13 true_13);
auth.email()
#
caution
Deprecated. Use auth.jwt() ->> 'email'
instead.
Returns the email of the user making the request.