--- id: 'row-level-security' title: 'Row Level Security' description: 'Secure your data using Postgres Row Level Security.' subtitle: 'Secure your data using Postgres Row Level Security.' --- When you need granular authorization rules, nothing beats Postgres's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html). ## Row Level Security in Supabase RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/auth/row-level-security) for end-to-end user security from the browser to the database. RLS is a Postgres primitive and can provide "[defense in depth]()" to protect your data from malicious actors even when accessed through third-party tooling. ## Policies [Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. 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 ... ```sql create policy "Individuals can view their own todos." on todos for select using ( auth.uid() = user_id ); ``` .. would translate to this whenever a user tries to select from the todos table: ```sql select * from todos where auth.uid() = todos.user_id; -- Policy is implicitly added. ``` ## Enabling Row Level Security You can enable RLS for any table using the `enable row level security` clause: ```sql alter table "table_name" enable row level security; ``` Once you have enabled RLS, no data will be accessible via the [API](/docs/guides/api) when using the public `anon` key, until you create policies. ## Creating policies Policies are simply SQL logic that you attach to a Postgres table. You can attach as many policies as you want to each table. Supabase provides some [helpers](/docs/guides/auth/row-level-security#helper-functions) that simplify RLS if you're using Supabase Auth. We'll use these helpers to illustrate some basic policies: ### SELECT policies You can specify select policies with the `using` clause. Let's say you have a table called `profiles` in the public schema and you want enable read access to everyone. ```sql -- 1. Create table create table profiles ( id uuid primary key, user_id references auth.users, avatar_url text ); -- 2. Enable RLS alter table profiles enable row level security; -- 3. Create Policy create policy "Public profiles are visible to everyone." on profiles for select to anon -- the Postgres Role (recommended) using ( true ); -- the actual Policy ``` Alternatively, if you only wanted users to be able to see their own profiles: ```sql create policy "User can see their own profile only." on profiles for select using ( auth.uid() = user_id ); ``` ### INSERT policies You can specify insert policies with the `with check` clause. The `with check` expression ensures that any new row data adheres to the policy constraints. Let's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert: ```sql -- 1. Create table create table profiles ( id uuid primary key, user_id references auth.users, avatar_url text ); -- 2. Enable RLS alter table profiles enable row level security; -- 3. Create Policy create policy "Users can create a profile." on profiles for insert to authenticated -- the Postgres Role (recommended) with check ( auth.uid() = user_id ); -- the actual Policy ``` ### UPDATE policies You can specify update policies by combining both the `using` and `with check` expressions. The `using` clause represents the condition that must be true for the update to be allowed, and `with check` clause ensures that the updates made adhere to the policy constraints. Let's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile. You can create a policy where the `using` clause checks if the user owns the profile being updated. And the `with check` clause ensures that, in the resultant row, users do not change the `user_id` to a value that is not equal to their User ID, maintaining that the modified profile still meets the ownership condition. ```sql -- 1. Create table create table profiles ( id uuid primary key, user_id references auth.users, avatar_url text ); -- 2. Enable RLS alter table profiles enable row level security; -- 3. Create Policy create policy "Users can update their own profile." on profiles for update to authenticated -- the Postgres Role (recommended) using ( auth.uid() = user_id ) -- checks if the existing row complies with the policy expression with check ( auth.uid() = user_id ); -- checks if the new row complies with the policy expression ``` If no `with check` expression is defined, then the `using` expression will be used both to determine which rows are visible (normal USING case) and which new rows will be allowed to be added (WITH CHECK case). ### DELETE policies You can specify delete policies with the `using` clause. Let's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile: ```sql -- 1. Create table create table profiles ( id uuid primary key, user_id references auth.users, avatar_url text ); -- 2. Enable RLS alter table profiles enable row level security; -- 3. Create Policy create policy "Users can delete a profile." on profiles for delete to authenticated -- the Postgres Role (recommended) using ( auth.uid() = user_id ); -- the actual Policy ``` ## Bypassing Row Level Security You can create [Postgres Roles](/docs/guides/database/postgres/roles) which can bypass Row Level Security using the "bypass RLS" privilege: ```sql grant bypassrls on "table_name" to "role_name"; ``` This can be useful for system-level access. You should _never_ share login credentials for any Postgres Role with this privilege. ## RLS performance recommendations Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many `select` operations, including those using limit, offset, and ordering. Based on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS: ### Add indexes Make sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this: ```sql create policy "rls_test_select" on test_table to authenticated using ( auth.uid() = user_id ); ``` You can add an index like: ```sql create index userid on test_table using btree (user_id); ``` #### Benchmarks | Test | Before (ms) | After (ms) | % Improvement | Change | | --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------- | | [test1-indexed](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test1-indexed) | 171 | < 0.1 | 99.94% |
Before:
No index

After:
`user_id` indexed
| ### Call functions with `select` You can use `select` statement to improve policies that use functions. For example, instead of this: ```sql create policy "rls_test_select" on test_table to authenticated using ( auth.uid() = user_id ); ``` You can do: ```sql create policy "rls_test_select" on test_table to authenticated using ( (select auth.uid()) = user_id ); ``` This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an `initPlan` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row. You can only use this technique if the results of the query or function do not change based on the row data. #### Benchmarks | Test | Before (ms) | After (ms) | % Improvement | Change | | --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [test2a-wrappedSQL-uid]() | 179 | 9 | 94.97% |
Before:
`auth.uid() = user_id`

After:
`(select auth.uid()) = user_id`
| | [test2b-wrappedSQL-isadmin]() | 11,000 | 7 | 99.94% |
Before:
`is_admin()` _table join_

After:
`(select is_admin())` _table join_
| | [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% |
Before:
`is_admin() OR auth.uid() = user_id`

After:
`(select is_admin()) OR (select auth.uid() = user_id)`
| | [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% |
Before:
`has_role() = role`

After:
(select has_role()) = role
| | [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% |
Before:
`team_id=any(user_teams())`

After:
team_id=any(array(select user_teams()))
| ### Add filters to every query Policies are "implicit where clauses," so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example): {/* prettier-ignore */} ```js const { data } = supabase .from('table') .select() ``` You should always add a filter: {/* prettier-ignore */} ```js const { data } = supabase .from('table') .select() .eq('user_id', userId) ``` Even though this duplicates the contents of the Policy, Postgres can use the filter to construct a better query plan. #### Benchmarks | Test | Before (ms) | After (ms) | % Improvement | Change | | ------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | [test3-addfilter](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test3-addfilter) | 171 | 9 | 94.74% |
Before:
`auth.uid() = user_id`

After:
add `.eq` or `where` on `user_id`
| ### Use security definer functions A "security definer" function runs using the same role that _created_ the function. This means that if you create a role with a superuser (like `postgres`), then that function will have `bypassrls` privileges. For example, if you had a policy like this: ```sql create policy "rls_test_select" on test_table to authenticated using ( exists ( select 1 from roles_table where auth.uid() = user_id and role = 'good_role' ) ); ``` We can instead create a `security definer` function which can scan `roles_table` without any RLS penalties: ```sql create function private.has_good_role() returns boolean language plpgsql security definer -- will run as the creator as $$ begin return exists ( select 1 from roles_table where auth.uid() = user_id and role = 'good_role' ); end; $$; -- Update our policy to use this function: create policy "rls_test_select" on test_table to authenticated using ( private.has_good_role() ); ``` Security-definer functions should never be created in a schema in the "Exposed schemas" inside your [API settings](https://supabase.com/dashboard/project/_/settings/api)`. ### Minimize joins You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an `IN` or `ANY` operation in your filter. For example, this is an example of a slow policy which joins the source `test_table` to the target `team_user`: ```sql create policy "rls_test_select" on test_table to authenticated using ( auth.uid() in ( select user_id from team_user where team_user.team_id = team_id -- joins to the source "test_table.team_id" ) ); ``` We can rewrite this to avoid this join, and instead select the filter criteria into a set: ```sql create policy "rls_test_select" on test_table to authenticated using ( team_id in ( select team_id from team_user where user_id = auth.uid() -- no join ) ); ``` In this case you can also consider [using a `security definer` function](#use-security-definer-functions) to bypass RLS on the join table: If the list exceeds 1000 items, a different approach may be needed or you may need to analyze the approach to ensure that the performance is acceptable. #### Benchmarks | Test | Before (ms) | After (ms) | % Improvement | Change | | --------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | [test5-fixed-join](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test5-fixed-join) | 9,000 | 20 | 99.78% |
Before:
`auth.uid()` in table join on col

After:
col in table join on `auth.uid()`
| ### Specify roles in your policies Always use the Role of inside your policies, specified by the `TO` operator. For example, instead of this query: ```sql create policy "rls_test_select" on rls_test using ( auth.uid() = user_id ); ``` Use: ```sql create policy "rls_test_select" on rls_test to authenticated using ( auth.uid() = user_id ); ``` This prevents the policy `( auth.uid() = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step. #### Benchmarks | Test | Before (ms) | After (ms) | % Improvement | Change | | --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- | | [test6-To-role](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test6-To-role) | 170 | < 0.1 | 99.78% |
Before:
No `TO` policy

After:
`TO authenticated` (anon accessing)
|