mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
Merge pull request #16704 from supabase/doc/rls_recommendations
Docs: RLS and Roles
This commit is contained in:
17 files changed
+1006
-588
No files matched your search
@@ -545,34 +545,35 @@ export const database: NavMenuConstant = {
|
||||
items: [
|
||||
{ name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres' },
|
||||
{ name: 'Managing tables, views, and data', url: '/guides/database/tables' },
|
||||
{ name: 'JSON and unstructured data', url: '/guides/database/json' },
|
||||
{ name: 'Managing database functions', url: '/guides/database/functions' },
|
||||
{ name: 'Managing indexes', url: '/guides/database/postgres/indexes' },
|
||||
{ name: 'Managing database triggers', url: '/guides/database/postgres/triggers' },
|
||||
{ name: 'Managing database webhooks', url: '/guides/database/webhooks' },
|
||||
{ name: 'Managing database replication', url: '/guides/database/replication' },
|
||||
{ name: 'Using Full Text Search', url: '/guides/database/full-text-search' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Access and security',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
|
||||
{ name: 'Managing Postgres Roles', url: '/guides/database/postgres/roles' },
|
||||
{ name: 'Managing secrets with Vault', url: '/guides/database/vault' },
|
||||
{ name: 'Column encryption', url: '/guides/database/column-encryption' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Postgres Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Analyzing efficiency and performance',
|
||||
url: '/guides/database/inspect',
|
||||
},
|
||||
{
|
||||
name: 'JSON and unstructured data',
|
||||
url: '/guides/database/json',
|
||||
},
|
||||
{ name: 'Implementing Full Text Search', url: '/guides/database/full-text-search' },
|
||||
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
|
||||
{ name: 'Implementing column encryption', url: '/guides/database/column-encryption' },
|
||||
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
|
||||
{ name: 'Query Optimization', url: '/guides/database/query-optimization' },
|
||||
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
|
||||
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
|
||||
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
|
||||
{ name: 'Managing database replication', url: '/guides/database/replication' },
|
||||
{ name: 'Testing your database', url: '/guides/database/testing' },
|
||||
{ name: 'Managing Timeouts', url: '/guides/database/timeouts' },
|
||||
{ name: 'Managing Passwords', url: '/guides/database/managing-passwords' },
|
||||
{ name: 'Configuring Timezones', url: '/guides/database/managing-timezones' },
|
||||
{ name: 'Database Configuration', url: '/guides/database/postgres/configuration' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -159,6 +159,7 @@ const Layout: FC<Props> = (props) => {
|
||||
<div className="border-l">
|
||||
{props.meta?.tocVideo && !!tocVideoPreview && (
|
||||
<div className="relative mb-6 pl-5">
|
||||
<h1>{props.meta.tocVideo}</h1>
|
||||
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,17 +8,11 @@ export const meta = {
|
||||
}
|
||||
|
||||
|
||||
Multi-factor authentication (MFA), sometimes called two-factor
|
||||
authentication (2FA), adds an additional layer of security to your
|
||||
application by verifying their identity through
|
||||
additional verification steps.
|
||||
Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), adds an additional layer of security to your application by verifying their identity through additional verification steps.
|
||||
|
||||
It is considered a best practice to use MFA for your applications.
|
||||
|
||||
Users with weak passwords or compromised social login accounts are
|
||||
prone to malicious account takeovers. These can be prevented with
|
||||
MFA because they require the user to provide proof of
|
||||
both of these:
|
||||
Users with weak passwords or compromised social login accounts are prone to malicious account takeovers. These can be prevented with MFA because they require the user to provide proof of both of these:
|
||||
|
||||
- Something they know.
|
||||
Password, or access to a social-login account.
|
||||
@@ -27,9 +21,7 @@ both of these:
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase Auth implements only Time-based One Time Factor(TOTP) multi-factor authentication. This type of
|
||||
multi-factor authentication uses a timed one-time password generated from an
|
||||
authenticator app in the control of users.
|
||||
Supabase Auth implements only Time-based One Time Password(TOTP) multi-factor authentication. This type of multi-factor authentication uses a timed one-time password generated from an authenticator app in the control of users.
|
||||
|
||||
Applications using MFA require two important flows:
|
||||
|
||||
@@ -44,8 +36,7 @@ Supabase Auth provides:
|
||||
- **Challenge and Verify APIs** - securely verify that the user has access to a factor.
|
||||
- **List Factors API** - build rich user interfaces for signing in with additional factors.
|
||||
|
||||
Below is a flow chart illustrating how these APIs work together to enable MFA
|
||||
features in your app.
|
||||
Below is a flow chart illustrating how these APIs work together to enable MFA features in your app.
|
||||
|
||||
<Mermaid
|
||||
chart={`
|
||||
@@ -59,18 +50,11 @@ graph TD;
|
||||
`}
|
||||
/>
|
||||
|
||||
These sets of APIs let you control the MFA experience that works for you. You
|
||||
can create flows where MFA is optional, mandatory for all or only specific
|
||||
groups of users.
|
||||
These sets of APIs let you control the MFA experience that works for you. You can create flows where MFA is optional, mandatory for all, or only specific groups of users.
|
||||
|
||||
Once users have enrolled or signed-in with a factor, Supabase Auth adds
|
||||
additional metadata to the user's access token (JWT) that your application can
|
||||
use to allow or deny access.
|
||||
Once users have enrolled or signed-in with a factor, Supabase Auth adds additional metadata to the user's access token (JWT) that your application can use to allow or deny access.
|
||||
|
||||
This information is represented by an [Authenticator Assurance
|
||||
Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), a
|
||||
standard measure about the assurance Supabase Auth has of the user's identity
|
||||
for that particular session. There are two levels recognized today:
|
||||
This information is represented by an [Authenticator Assurance Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), a standard measure about the assurance of the user's identity Supabase Auth has for that particular session. There are two levels recognized today:
|
||||
|
||||
1. **Assurance Level 1: `aal1`**
|
||||
Means that the user's identity was verified using a conventional login method
|
||||
@@ -80,10 +64,7 @@ for that particular session. There are two levels recognized today:
|
||||
Means that the user's identity was additionally verified using at least one
|
||||
second factor, such as a TOTP code.
|
||||
|
||||
This assurance level is encoded in the `aal` claim in the JWT associated with
|
||||
the user. By decoding this value you can create custom authorization rules in
|
||||
your frontend, backend and database that will enforce the MFA policy that works
|
||||
for your application. JWTs without an `aal` claim are at the `aal1` level.
|
||||
This assurance level is encoded in the `aal` claim in the JWT associated with the user. By decoding this value you can create custom authorization rules in your frontend, backend, and database that will enforce the MFA policy that works for your application. JWTs without an `aal` claim are at the `aal1` level.
|
||||
|
||||
## Adding to your app
|
||||
|
||||
@@ -107,8 +88,7 @@ Adding MFA to your app involves these four steps:
|
||||
|
||||
### Add enrollment flow
|
||||
|
||||
An enrollment flow provides a UI for users to set up additional authentication factors.
|
||||
Most applications add the enrollment flow in two places within their app:
|
||||
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
|
||||
|
||||
1. Right after login or sign up.
|
||||
This lets users quickly set up MFA immediately after they log in or create an
|
||||
@@ -118,8 +98,7 @@ Most applications add the enrollment flow in two places within their app:
|
||||
2. From within a settings page.
|
||||
Allows users to set up, disable or modify their MFA settings.
|
||||
|
||||
We recommend building one generic flow that you can reuse in both cases with
|
||||
minor modifications.
|
||||
We recommend building one generic flow that you can reuse in both cases with minor modifications.
|
||||
|
||||
Enrolling a factor for use with MFA takes three steps:
|
||||
|
||||
@@ -140,8 +119,7 @@ Enrolling a factor for use with MFA takes three steps:
|
||||
|
||||
#### Example: React
|
||||
|
||||
Below is an example that creates a new `EnrollMFA` component that illustrates
|
||||
the important pieces of the MFA enrollment flow.
|
||||
Below is an example that creates a new `EnrollMFA` component that illustrates the important pieces of the MFA enrollment flow.
|
||||
|
||||
- When the component appears on screen, the `supabase.auth.mfa.enroll()` API is
|
||||
called once to start the process of enrolling a new factor for the current
|
||||
@@ -240,18 +218,14 @@ export function EnrollMFA({
|
||||
|
||||
### Add unenrollment flow
|
||||
|
||||
An unenrollment flow provides a UI for users to manage and unenroll factors linked to their accounts.
|
||||
Most applications do so via a factor management page where users can view and unlink selected factors.
|
||||
An unenrollment flow provides a UI for users to manage and unenroll factors linked to their accounts. Most applications do so via a factor management page where users can view and unlink selected factors.
|
||||
|
||||
When a user unenrolls a factor, call `supabase.auth.mfa.unenroll()` with the ID of the factor.
|
||||
For example, call `supabase.auth.mfa.unenroll({factorId: "d30fd651-184e-4748-a928-0a4b9be1d429"})` to unenroll a factor with ID `d30fd651-184e-4748-a928-0a4b9be1d429`.
|
||||
When a user unenrolls a factor, call `supabase.auth.mfa.unenroll()` with the ID of the factor. For example, call `supabase.auth.mfa.unenroll({factorId: "d30fd651-184e-4748-a928-0a4b9be1d429"})` to unenroll a factor with ID `d30fd651-184e-4748-a928-0a4b9be1d429`.
|
||||
|
||||
|
||||
#### Example: React
|
||||
|
||||
Below is an example that creates a new `UnenrollMFA` component that illustrates
|
||||
the important pieces of the MFA enrollment flow. Note that users can only unenroll a factor after completing the enrollment flow and obtaining
|
||||
an `aal2` JWT claim.Here are some points of note:
|
||||
Below is an example that creates a new `UnenrollMFA` component that illustrates the important pieces of the MFA enrollment flow. Note that users can only unenroll a factor after completing the enrollment flow and obtaining an `aal2` JWT claim. Here are some points of note:
|
||||
|
||||
- When the component appears on screen, the `supabase.auth.mfa.listFactors()` endpoint
|
||||
fetches all existing factors together with their details.
|
||||
@@ -317,17 +291,11 @@ export function UnenrollMFA() {
|
||||
|
||||
### Add challenge step to login
|
||||
|
||||
Once a user has logged in via their first factor (email+password, magic link,
|
||||
one time password, social login...) you need to perform a check if any
|
||||
additional factors need to be verified.
|
||||
Once a user has logged in via their first factor (email+password, magic link, one time password, social login etc.) you need to perform a check if any additional factors need to be verified.
|
||||
|
||||
This can be done by using the
|
||||
`supabase.auth.mfa.getAuthenticatorAssuranceLevel()` API. When the user signs
|
||||
in and is redirected back to your app, you should call this method to extract
|
||||
the user's current and next authenticator assurance level (AAL).
|
||||
This can be done by using the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` API. When the user signs in and is redirected back to your app, you should call this method to extract the user's current and next authenticator assurance level (AAL).
|
||||
|
||||
Therefore if you receive a `currentLevel` which is `aal1` but a `nextLevel`
|
||||
of `aal2`, the user should be given the option to go through MFA.
|
||||
Therefore if you receive a `currentLevel` which is `aal1` but a `nextLevel` of `aal2`, the user should be given the option to go through MFA.
|
||||
|
||||
Below is a table that explains the combined meaning.
|
||||
|
||||
@@ -340,14 +308,9 @@ Below is a table that explains the combined meaning.
|
||||
|
||||
#### Example: React
|
||||
|
||||
Adding the challenge step to login depends heavily on the architecture of your
|
||||
app. However, a fairly common way to structure React apps is to have a large
|
||||
component (often named `App`) which contains most of the authenticated
|
||||
application logic.
|
||||
Adding the challenge step to login depends heavily on the architecture of your app. However, a fairly common way to structure React apps is to have a large component (often named `App`) which contains most of the authenticated application logic.
|
||||
|
||||
This example will wrap this component with logic that will show an MFA
|
||||
challenge screen if necessary, before showing the full application. This is
|
||||
illustrated in the `AppWithMFA` example below.
|
||||
This example will wrap this component with logic that will show an MFA challenge screen if necessary, before showing the full application. This is illustrated in the `AppWithMFA` example below.
|
||||
|
||||
```tsx
|
||||
function AppWithMFA() {
|
||||
@@ -468,12 +431,9 @@ function AuthMFA() {
|
||||
|
||||
### Enforce rules for MFA logins
|
||||
|
||||
Adding MFA to your app's UI does not in-and-of-itself offer a higher level of
|
||||
security to your users. You also need to enforce the MFA rules in your
|
||||
application's database, APIs and server-side rendering.
|
||||
Adding MFA to your app's UI does not in-and-of-itself offer a higher level of security to your users. You also need to enforce the MFA rules in your application's database, APIs, and server-side rendering.
|
||||
|
||||
Depending on your application's needs, there are three ways you can choose to
|
||||
enforce MFA.
|
||||
Depending on your application's needs, there are three ways you can choose to enforce MFA.
|
||||
|
||||
1. **Enforce for all users (new and existing).**
|
||||
Any user account will have to enroll MFA to continue using your app.
|
||||
@@ -489,21 +449,17 @@ enforce MFA.
|
||||
|
||||
#### Database
|
||||
|
||||
Your app should sufficiently deny or allow access to tables or rows based on
|
||||
the user's current and possible authenticator levels.
|
||||
Your app should sufficiently deny or allow access to tables or rows based on the user's current and possible authenticator levels.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
PostgreSQL has two types of policies: permissive and restrictive. This guide
|
||||
uses restrictive policies. Make sure you don't omit the `as restrictive`
|
||||
clause.
|
||||
PostgreSQL has two types of policies: permissive and restrictive. This guide uses restrictive policies. Make sure you don't omit the `as restrictive` clause.
|
||||
|
||||
</Admonition>
|
||||
|
||||
##### Enforce for all users (new and existing)
|
||||
|
||||
If your app falls under this case, this is a template Row Level Security policy
|
||||
you can apply to all your tables:
|
||||
If your app falls under this case, this is a template Row Level Security policy you can apply to all your tables:
|
||||
|
||||
```sql
|
||||
create policy "Policy name."
|
||||
@@ -520,9 +476,7 @@ create policy "Policy name."
|
||||
|
||||
##### Enforce for new users only
|
||||
|
||||
If your app falls under this case, the rules get more complex. User accounts
|
||||
created past a certain timestamp must have a `aal2` level to access the
|
||||
database.
|
||||
If your app falls under this case, the rules get more complex. User accounts created past a certain timestamp must have a `aal2` level to access the database.
|
||||
|
||||
```sql
|
||||
create policy "Policy name."
|
||||
@@ -582,23 +536,15 @@ create policy "Policy name."
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
When using the Supabase JavaScript library in a server-side rendering context,
|
||||
make sure you always create a new object for each request! This will prevent
|
||||
you from accidentally rendering and serving content belonging to different
|
||||
users.
|
||||
When using the Supabase JavaScript library in a server-side rendering context, make sure you always create a new object for each request! This will prevent you from accidentally rendering and serving content belonging to different users.
|
||||
|
||||
</Admonition>
|
||||
|
||||
It is possible to enforce MFA on the Server-Side Rendering level. However, this
|
||||
can be tricky do to well.
|
||||
It is possible to enforce MFA on the Server-Side Rendering level. However, this can be tricky do to well.
|
||||
|
||||
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and
|
||||
`supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session
|
||||
and any factors that are enabled for a user, similar to how you would use these
|
||||
on the browser.
|
||||
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and `supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session and any factors that are enabled for a user, similar to how you would use these on the browser.
|
||||
|
||||
However, encountering a different AAL level on the server may not actually be a
|
||||
security problem. Consider these likely scenarios:
|
||||
However, encountering a different AAL level on the server may not actually be a security problem. Consider these likely scenarios:
|
||||
|
||||
1. User signed-in with a conventional method but closed their tab on the MFA
|
||||
flow.
|
||||
@@ -607,16 +553,11 @@ security problem. Consider these likely scenarios:
|
||||
3. User has lost their authenticator device and is confused about the next
|
||||
steps.
|
||||
|
||||
We thus recommend you redirect users to a page where they can authenticate
|
||||
using their additional factor, instead of rendering a HTTP 401 Unauthorized or
|
||||
HTTP 403 Forbidden content.
|
||||
We thus recommend you redirect users to a page where they can authenticate using their additional factor, instead of rendering a HTTP 401 Unauthorized or HTTP 403 Forbidden content.
|
||||
|
||||
### APIs
|
||||
|
||||
If your application uses the Supabase Database, Storage or Edge Functions, just
|
||||
using Row Level Security policies will give you sufficient protection. In the
|
||||
event that you have other APIs that you wish to protect, follow these general
|
||||
guidelines:
|
||||
If your application uses the Supabase Database, Storage or Edge Functions, just using Row Level Security policies will give you sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines:
|
||||
|
||||
1. **Use a good JWT verification and parsing library for your language.**
|
||||
This will let you securely parse JWTs and extract their claims.
|
||||
@@ -632,28 +573,17 @@ guidelines:
|
||||
|
||||
### Why is there a challenge and verify API when challenge does not do much?
|
||||
|
||||
TOTP is not going to be the only MFA factor Supabase Auth is going to support
|
||||
in the future. By separating out the challenge and verify steps, we're making
|
||||
the library forward compatible with new factors we may add in the future --
|
||||
such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would
|
||||
actually send out the SMS with the authentication code.
|
||||
TOTP is not going to be the only MFA factor Supabase Auth is going to support in the future. By separating out the challenge and verify steps, we're making the library forward compatible with new factors we may add in the future -- such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would actually send out the SMS with the authentication code.
|
||||
|
||||
### What's inside the QR code?
|
||||
|
||||
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially
|
||||
introduced by Google
|
||||
Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
|
||||
but is now universally accepted by all authenticator apps.
|
||||
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially introduced by Google Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) but is now universally accepted by all authenticator apps.
|
||||
|
||||
### How do I check _when_ a user went through MFA?
|
||||
|
||||
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods
|
||||
Reference) claim. It is an array of objects that indicate what authentication
|
||||
methods the user has used so far.
|
||||
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods Reference) claim. It is an array of objects that indicate what authentication methods the user has used so far.
|
||||
|
||||
For example, the following structure describes a user that first signed in with
|
||||
a password-based method, and then went through TOTP MFA 2 minutes and 12
|
||||
seconds later. The entries are ordered most recent method first!
|
||||
For example, the following structure describes a user that first signed in with a password-based method, and then went through TOTP MFA 2 minutes and 12 seconds later. The entries are ordered most recent method first!
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -670,8 +600,7 @@ seconds later. The entries are ordered most recent method first!
|
||||
}
|
||||
```
|
||||
|
||||
Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy
|
||||
access to this information in your browser app.
|
||||
Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy access to this information in your browser app.
|
||||
|
||||
You can use this PostgreSQL snippet in RLS policies, too:
|
||||
|
||||
@@ -686,8 +615,7 @@ json_query_path(auth.jwt(), '$.amr[0]')
|
||||
- `$.amr[0]` is a SQL/JSON path expression that fetches the most recent
|
||||
authentication method in the JWT.
|
||||
|
||||
Once you have extracted the most recent entry in the array, you can compare the
|
||||
`method` and `timestamp` to enforce stricter rules. For instance, you can mandate that access will be only be granted on a table to users who have recently signed in with a password.
|
||||
Once you have extracted the most recent entry in the array, you can compare the `method` and `timestamp` to enforce stricter rules. For instance, you can mandate that access will be only be granted on a table to users who have recently signed in with a password.
|
||||
|
||||
Currently recognized authentication methods are:
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'row-level-security',
|
||||
title: 'Row Level Security',
|
||||
description: 'Secure your data using Postgres Row Level Security.',
|
||||
video: 'https://www.youtube.com/v/Ow_Uzedfohk',
|
||||
description: 'Using Row Level Security with Supabase Auth.',
|
||||
subtitle: 'Using Row Level Security with Supabase Auth.',
|
||||
tocVideo: 'Ow_Uzedfohk',
|
||||
}
|
||||
|
||||
When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).
|
||||
Supabase Auth is designed to work perfectly with [Postgres Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS).
|
||||
|
||||
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
|
||||
You can use RLS to create [Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) that are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
@@ -27,8 +28,8 @@ You can just think of them as adding a `WHERE` clause to every query. For exampl
|
||||
|
||||
```sql
|
||||
create policy "Individuals can view their own todos."
|
||||
on todos for select
|
||||
using ( auth.uid() = user_id );
|
||||
on todos for select
|
||||
using ( auth.uid() = user_id );
|
||||
```
|
||||
|
||||
.. would translate to this whenever a user tries to select from the todos table:
|
||||
@@ -39,9 +40,33 @@ from todos
|
||||
where auth.uid() = todos.user_id; -- Policy is implicitly added.
|
||||
```
|
||||
|
||||
## Authenticated and anonymous roles
|
||||
|
||||
Supabase Auth maps every request to one of the roles:
|
||||
|
||||
- `anon`: an anonymous request (the user is not logged in)
|
||||
- `authenticated`: an authenticated request (the user is logged in)
|
||||
|
||||
These are actually [Postgres Roles](/docs/guides/database/postgres/roles), and so they have significant value for the [performance](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) of your RLS Policies. You can use these roles within your Policies using the `TO` clause:
|
||||
|
||||
```sql
|
||||
create policy "Profiles are viewable by everyone"
|
||||
on profiles for select
|
||||
to authenticated, anon
|
||||
using ( true );
|
||||
|
||||
-- OR
|
||||
|
||||
create policy "Public profiles are viewable only by authenticated users"
|
||||
on profiles for select
|
||||
to authenticated
|
||||
using ( true );
|
||||
```
|
||||
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Supabase provides you with a few easy functions that you can use with your policies.
|
||||
Supabase provides some helper functions that make it easier to write Policies.
|
||||
|
||||
### `auth.uid()`
|
||||
|
||||
@@ -49,80 +74,117 @@ Returns the ID of the user making the request.
|
||||
|
||||
### `auth.jwt()`
|
||||
|
||||
Returns the JWT of the user making the request.
|
||||
Returns the JWT of the user making the request. Anything that you store in the user's `app_metadata` column or the `user_metadata` column will be accessible using this function. It's important to know the distinction between these two:
|
||||
|
||||
## Examples
|
||||
- `user_metadata` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.
|
||||
- `app_metadata` - cannot be updated by the user, so it's a good place to store authorization data.
|
||||
|
||||
Here are some examples to show you the power of PostgreSQL's RLS.
|
||||
The `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:
|
||||
|
||||
### Allow read access
|
||||
|
||||
```sql
|
||||
-- 1. Create table
|
||||
create table profiles (
|
||||
id uuid references auth.users,
|
||||
avatar_url text
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table profiles
|
||||
enable row level security;
|
||||
|
||||
-- 3. Create Policy
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select using (
|
||||
true
|
||||
);
|
||||
```sql
|
||||
create policy "User is in team"
|
||||
on my_table
|
||||
to authenticated
|
||||
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
|
||||
```
|
||||
|
||||
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.
|
||||
{/* prettier-ignore */}
|
||||
<Admonition type="caution">
|
||||
Keep in mind that a JWT is not always "fresh". In the example above, even if you remove a user from a team and update the `app_metadata` field, that will not be reflected using `auth.jwt()` until the user's JWT is refreshed.
|
||||
|
||||
### Restrict updates
|
||||
Also, if you are using Cookies for Auth, then you must be mindful of the JWT size. Some browsers are limited to 4096 bytes for each cookie, and so the total size of your JWT should be small enough to fit inside this limitation.
|
||||
</Admonition>
|
||||
|
||||
## Important considerations
|
||||
|
||||
We recommend reading the [Row Level Security](/docs/guides/database/postgres/row-level-security) guide in the database section to learn more about Postgres RLS. When using Postgres on Supabase there are some important things to keep in mind to maintain data security.
|
||||
|
||||
### Never use a service key on the client
|
||||
|
||||
Supabase provides special "Service" keys, which can be used to bypass RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Supabase will adhere to the RLS policy of the signed-in user, even if the client library is initialized with a Service Key.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Always enable RLS on public tables
|
||||
|
||||
You should _always_ enable RLS on tables created in a public schema. This is considered "default safe". Unfortunately this is not enabled by default on Postgres, so you will need to keep this in mind - especially if you are using the SQL Editor or database migrations. RLS is already enabled by default if you create a table using the Table Editor. If you want to allow public access to a table, just add a Policy with `true`:
|
||||
|
||||
```sql
|
||||
create policy "Allow public access"
|
||||
on my_table for select
|
||||
using ( true );
|
||||
```
|
||||
|
||||
### Using external authorization systems
|
||||
|
||||
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 take this path, don't put your tables in the `public` schema - instead create a new schema for your tables and functions:
|
||||
|
||||
```sql
|
||||
create schema private;
|
||||
|
||||
create table private.employees (
|
||||
id serial primary key,
|
||||
name text
|
||||
);
|
||||
```
|
||||
|
||||
If you do put anything in the `public` schema, make sure to enable RLS (you don't need to add any policies):
|
||||
|
||||
```sql
|
||||
-- 1. Create table
|
||||
create table profiles (
|
||||
id uuid references auth.users,
|
||||
avatar_url text
|
||||
id serial primary key,
|
||||
email text
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table profiles
|
||||
enable row level security;
|
||||
|
||||
-- 3. Create Policy
|
||||
create policy "Users can update their own profiles."
|
||||
on profiles for update using (
|
||||
auth.uid() = id
|
||||
);
|
||||
alter table profiles enable row level security;
|
||||
```
|
||||
|
||||
This makes the tables inaccessible via the [APIs](/docs/guides/api).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Row Level Security is extremely versatile, since it simply uses SQL to express access rules for your data.
|
||||
|
||||
### Using functions
|
||||
|
||||
You can use any Postgres function inside a Policy. The [Helper Functions](#helper-functions) above are simply Postgres functions we've made available in the `auth` schema. This is an example which:
|
||||
|
||||
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
|
||||
3. Creates a policy which allows logged in users to update their own data, using the `auth.uid()` function.
|
||||
|
||||
```sql
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
to authenticated, anon
|
||||
-- 1. Create table
|
||||
create table profiles (
|
||||
id uuid 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 profiles"
|
||||
on profiles for update
|
||||
to authenticated
|
||||
using (
|
||||
true
|
||||
auth.uid() = id
|
||||
);
|
||||
```
|
||||
|
||||
### Policies with joins
|
||||
**Note:** If you want to use [upsert](/docs/reference/javascript/upsert) operations, the user needs to have `INSERT`, `UPDATE`, and `SELECT` permissions.
|
||||
|
||||
Policies can even include table joins. This example shows how you can query "external" tables to build more advanced rules.
|
||||
### Using joins
|
||||
|
||||
Policies can include table joins. This example shows how you can query "external" tables to build more advanced rules. RLS policies are executed on every access of the table, so be careful to make sure that policies are efficient.
|
||||
|
||||
```sql
|
||||
-- 1. Create a table of teams
|
||||
create table teams (
|
||||
id serial primary key,
|
||||
name text
|
||||
@@ -135,11 +197,10 @@ create table members (
|
||||
);
|
||||
|
||||
-- 3. Enable RLS
|
||||
alter table teams
|
||||
enable row level security;
|
||||
alter table teams enable row level security;
|
||||
|
||||
-- 4. Create Policy
|
||||
create policy "Team members can update team details if they belong to the team."
|
||||
create policy "Team members can update team details if they belong to the team"
|
||||
on teams
|
||||
for update using (
|
||||
auth.uid() in (
|
||||
@@ -149,331 +210,80 @@ create policy "Team members can update team details if they belong to the team."
|
||||
);
|
||||
```
|
||||
|
||||
**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.
|
||||
An important note here: 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. Another alternative is to use a "security definer" function which is created by a user with [bypassrls](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) privileges.
|
||||
|
||||
### Policies with security definer functions
|
||||
### Using 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.
|
||||
You can use `security definer` functions inside Policies. This is useful in a many-to-many relationships, and [important for performance](/docs/guides/database/postgres/row-level-security#use-security-definer-functions). 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.
|
||||
|
||||
```sql
|
||||
-- 1. Follow example for 'Policies with joins' above
|
||||
-- 1. Create a table of teams
|
||||
create table teams (
|
||||
id serial primary key,
|
||||
name text
|
||||
);
|
||||
|
||||
-- 2. Create many to many join
|
||||
create table members (
|
||||
team_id bigint references teams,
|
||||
user_id uuid references auth.users
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table members
|
||||
enable row level security
|
||||
alter table teams enable row level security;
|
||||
alter table members enable row level security
|
||||
|
||||
-- 3. Create security definer function
|
||||
create or replace function get_teams_for_authenticated_user()
|
||||
-- 3. Create security definer function, which should be run as "postgres"
|
||||
create function private.get_teams_for_authenticated_user()
|
||||
returns setof bigint
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
stable
|
||||
as $$
|
||||
select team_id
|
||||
from members
|
||||
where user_id = auth.uid()
|
||||
select team_id
|
||||
from members
|
||||
where user_id = auth.uid()
|
||||
$$;
|
||||
|
||||
-- 4. Create Policy
|
||||
create policy "Team members can update team members if they belong to the team."
|
||||
on members
|
||||
for all using (
|
||||
team_id in (
|
||||
select get_teams_for_authenticated_user()
|
||||
)
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```sql
|
||||
-- 1. Create table
|
||||
create table leaderboard (
|
||||
id uuid references auth.users,
|
||||
high_score bigint
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table leaderboard
|
||||
enable row level security;
|
||||
|
||||
-- 3. Create Policy
|
||||
create policy "Only Blizzard staff can update leaderboard"
|
||||
on leaderboard
|
||||
for update using (
|
||||
right(auth.jwt() ->> 'email', 13) = '@blizzard.com'
|
||||
);
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```sql
|
||||
-- 1. Create table
|
||||
create table if not exists stories (
|
||||
id uuid not null primary key DEFAULT uuid_generate_v4(),
|
||||
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
|
||||
content text not null
|
||||
);
|
||||
|
||||
-- 2. Enable RLS
|
||||
alter table stories
|
||||
enable row level security;
|
||||
|
||||
-- 3. Create Policy
|
||||
create policy "Stories are live for a day"
|
||||
on stories
|
||||
for select using (
|
||||
created_at > (current_timestamp - interval '1 day')
|
||||
);
|
||||
```
|
||||
|
||||
### 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.)
|
||||
|
||||
```sql
|
||||
create table posts (
|
||||
id serial primary key,
|
||||
creator_id uuid not null references auth.users(id),
|
||||
title text not null,
|
||||
body text not null,
|
||||
publish_date date not null default now(),
|
||||
audience uuid[] null -- many to many table omitted for brevity
|
||||
);
|
||||
|
||||
create table comments (
|
||||
id serial primary key,
|
||||
post_id int not null references posts(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id),
|
||||
body text not null,
|
||||
comment_date date not null default now()
|
||||
);
|
||||
|
||||
create policy "Creator can see their own posts"
|
||||
on posts
|
||||
for select
|
||||
using (
|
||||
auth.uid() = posts.creator_id
|
||||
);
|
||||
|
||||
create policy "Logged in users can see the posts if they belong to the post 'audience'."
|
||||
on posts
|
||||
for select
|
||||
using (
|
||||
auth.uid() = any (posts.audience)
|
||||
);
|
||||
|
||||
create policy "Users can see all comments for posts they have access to."
|
||||
on comments
|
||||
for select
|
||||
using (
|
||||
exists (
|
||||
select 1 from posts
|
||||
where posts.id = comments.post_id
|
||||
on members
|
||||
for all using (
|
||||
team_id in (
|
||||
select private.get_teams_for_authenticated_user()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Tips
|
||||
### Using built-in functions
|
||||
|
||||
### You don't have to use policies
|
||||
Postgres has a number of built-in functions. Most commonly you'll use `in()` and `any()` which will match a column's value to a list of values.
|
||||
|
||||
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:
|
||||
You can use any Postgres functions inside Policies. For example, we can use the `right(string, n)` function to match email domains:
|
||||
|
||||
```sql
|
||||
create table profiles (
|
||||
id serial primary key,
|
||||
email text
|
||||
);
|
||||
|
||||
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.
|
||||
<Admonition type="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.
|
||||
</Admonition>
|
||||
|
||||
### Testing policies
|
||||
|
||||
To test policies on the database itself (i.e., from the [SQL Editor](https://supabase.com/dashboard/project/_/sql) or from `psql`) without switching to your frontend and logging in as different users, you can utilize the following helper SQL procedures ([credits](https://github.com/supabase/supabase/issues/7311#issuecomment-1398648114)):
|
||||
|
||||
```sql
|
||||
grant anon, authenticated to postgres;
|
||||
|
||||
create or replace procedure auth.login_as_user (user_email text)
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
auth_user auth.users;
|
||||
begin
|
||||
select
|
||||
* into auth_user
|
||||
from
|
||||
auth.users
|
||||
where
|
||||
email = user_email;
|
||||
execute format('set request.jwt.claim.sub=%L', (auth_user).id::text);
|
||||
execute format('set request.jwt.claim.role=%I', (auth_user).role);
|
||||
execute format('set request.jwt.claim.email=%L', (auth_user).email);
|
||||
execute format('set request.jwt.claims=%L', json_strip_nulls(json_build_object('app_metadata', (auth_user).raw_app_meta_data))::text);
|
||||
|
||||
raise notice '%', format( 'set role %I; -- logging in as %L (%L)', (auth_user).role, (auth_user).id, (auth_user).email);
|
||||
execute format('set role %I', (auth_user).role);
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace procedure auth.login_as_anon ()
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
set request.jwt.claim.sub='';
|
||||
set request.jwt.claim.role='';
|
||||
set request.jwt.claim.email='';
|
||||
set request.jwt.claims='';
|
||||
set role anon;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace procedure auth.logout ()
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
set request.jwt.claim.sub='';
|
||||
set request.jwt.claim.role='';
|
||||
set request.jwt.claim.email='';
|
||||
set request.jwt.claims='';
|
||||
set role postgres;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
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](/docs/guides/database/extensions/pgtap) unit tests for policies.
|
||||
|
||||
<details>
|
||||
<summary>Click here to see an example `psql` interaction using this.</summary>
|
||||
|
||||
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:
|
||||
|
||||
```shell
|
||||
postgres=> select id, email from auth.users;
|
||||
id | email
|
||||
--------------------------------------+-------------------
|
||||
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | user1@example.com
|
||||
15d6811a-16ee-4fa2-9b18-b63085688be4 | user2@example.com
|
||||
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | user3@example.com
|
||||
(3 rows)
|
||||
|
||||
postgres=> table public.profiles;
|
||||
id | updated_at | username | full_name | avatar_url | website
|
||||
--------------------------------------+------------+----------+-----------+------------+---------
|
||||
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
|
||||
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
|
||||
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
|
||||
(3 rows)
|
||||
|
||||
postgres=> call auth.login_as_anon();
|
||||
CALL
|
||||
postgres=> update public.profiles set updated_at=now();
|
||||
UPDATE 0 -- anon users cannot update any profile but see all of them
|
||||
postgres=> table public.profiles;
|
||||
id | updated_at | username | full_name | avatar_url | website
|
||||
--------------------------------------+------------+----------+-----------+------------+---------
|
||||
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
|
||||
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
|
||||
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
|
||||
(3 rows)
|
||||
|
||||
postgres=> call auth.logout();
|
||||
CALL
|
||||
postgres=> call auth.login_as_user('user1@example.com');
|
||||
NOTICE: set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com')
|
||||
CALL
|
||||
postgres=> update public.profiles set updated_at=now();
|
||||
UPDATE 1 -- authenticated users can update their own profile and see all of them
|
||||
postgres=> table public.profiles;
|
||||
id | updated_at | username | full_name | avatar_url | website
|
||||
--------------------------------------+-------------------------------+----------+-----------+------------+---------
|
||||
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user1 | User 1 | |
|
||||
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user2 | User 2 | |
|
||||
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:39:16.204612+00 | user3 | User 3 | |
|
||||
(3 rows)
|
||||
|
||||
postgres=> call auth.logout();
|
||||
CALL
|
||||
postgres=> update public.profiles set updated_at=now();
|
||||
UPDATE 3 -- the 'postgres' role can update and see all profiles
|
||||
postgres=> table public.profiles;
|
||||
id | updated_at | username | full_name | avatar_url | website
|
||||
--------------------------------------+-------------------------------+----------+-----------+------------+---------
|
||||
15d6811a-16ee-4fa2-9b18-b63085688be4 | 2023-02-18 21:40:08.216324+00 | user1 | User 1 | |
|
||||
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | 2023-02-18 21:40:08.216324+00 | user2 | User 2 | |
|
||||
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:40:08.216324+00 | user3 | User 3 | |
|
||||
(3 rows)
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Deprecated features
|
||||
|
||||
We have deprecate some functions to ensure better performance and extensibilty of RLS policies.
|
||||
|
||||
### `auth.role()`
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
Deprecated: The `auth.role()` function has been deprecated in favour of using the `TO` field, natively supported within Postgres.
|
||||
|
||||
</Admonition>
|
||||
|
||||
```sql
|
||||
-- DEPRECATED
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select using (
|
||||
auth.role() = 'authenticated' or auth.role() = 'anon'
|
||||
);
|
||||
|
||||
-- RECOMMENDED
|
||||
create policy "Public profiles are viewable by everyone."
|
||||
on profiles for select
|
||||
to authenticated, anon
|
||||
using (
|
||||
true
|
||||
create policy "Only Supabase staff can update the leaderboard"
|
||||
on leaderboard
|
||||
to authenticated
|
||||
for update using (
|
||||
right(auth.jwt() ->> 'email', 13) = '@supabase.com'
|
||||
);
|
||||
```
|
||||
|
||||
### `auth.email()`
|
||||
### Using Multi-factor Authentication
|
||||
|
||||
<Admonition type="caution">
|
||||
RLS can be [combined with Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins) in Supabase Auth. For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
|
||||
|
||||
Deprecated. Use `auth.jwt() ->> 'email'` instead.
|
||||
|
||||
</Admonition>
|
||||
|
||||
Returns the email of the user making the request.
|
||||
```sql
|
||||
create policy "Restrict updates."
|
||||
on profiles
|
||||
as restrictive
|
||||
for update
|
||||
to authenticated using (
|
||||
auth.jwt()->>'aal' = 'aal2'
|
||||
);
|
||||
```
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -3,25 +3,24 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
export const meta = {
|
||||
id: 'database',
|
||||
title: 'Database',
|
||||
description: 'Use Supabase to manage your data.',
|
||||
description: 'Using Supabase to manage your data.',
|
||||
subtitle: 'Using Supabase to manage your data.',
|
||||
sidebar_label: 'Overview',
|
||||
}
|
||||
|
||||
Every Supabase project comes with a full [Postgres](https://www.postgresql.org/) database, a free and open source
|
||||
database which is considered one of the world's most stable and advanced databases.
|
||||
Every Supabase project comes with a full [Postgres](https://www.postgresql.org/) database, a free and open source database which is considered one of the world's most stable and advanced databases.
|
||||
|
||||
## Postgres or PostgreSQL?
|
||||
|
||||
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986.
|
||||
This package included a query language called "PostQUEL".
|
||||
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986. This package included a query language called "PostQUEL".
|
||||
|
||||
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL.
|
||||
Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
|
||||
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL. Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
|
||||
|
||||
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about
|
||||
simplicity, so we also refer to it as Postgres.
|
||||
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about simplicity, so we also refer to it as Postgres.
|
||||
|
||||
## Features
|
||||
## Supabase Studio
|
||||
|
||||
Postgres is known to be one of the most extensible databases available, so you can use it to build almost any application. The Supabase platform makes this easier with a simple-to-use dashboard called "Supabase Studio".
|
||||
|
||||
### Table View
|
||||
|
||||
@@ -71,16 +70,13 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Database backups **do not** include objects stored via the Storage API, as the database only
|
||||
includes metadata about these objects. Restoring an old backup does not restore objects that have
|
||||
been deleted since then.
|
||||
Database backups **do not** include objects stored via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects that have been deleted since then.
|
||||
|
||||
</Admonition>
|
||||
|
||||
### Extensions
|
||||
|
||||
To expand the functionality of your Postgres database, you can use extensions.
|
||||
You can enable Postgres extensions with the click of a button within the Supabase dashboard.
|
||||
To expand the functionality of your Postgres database, you can use extensions. You can enable Postgres extensions with the click of a button within the Supabase dashboard.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
@@ -91,10 +87,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
|
||||
|
||||
[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase.
|
||||
|
||||
## Tips
|
||||
|
||||
Read about resetting your database password [here](/docs/guides/database/managing-passwords) and changing the timezone of your server [here](/docs/guides/database/managing-timezones).
|
||||
|
||||
## Next steps
|
||||
|
||||
- Read more about [Postgres](https://www.postgresql.org/about/)
|
||||
|
||||
@@ -4,7 +4,8 @@ export const meta = {
|
||||
id: 'full-text-search',
|
||||
title: 'Full Text Search',
|
||||
description: 'How to use full text search in PostgreSQL.',
|
||||
video: 'https://www.youtube.com/v/b-mgca_2Oe4',
|
||||
subtitle: 'How to use full text search in PostgreSQL.',
|
||||
tocVideo: 'b-mgca_2Oe4',
|
||||
}
|
||||
|
||||
Postgres has built-in functions to handle `Full Text Search` queries. This is like a "search engine" within Postgres.
|
||||
|
||||
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'json',
|
||||
title: 'Analyzing efficiency and performance from the terminal',
|
||||
title: 'Debugging and monitoring',
|
||||
description:
|
||||
'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.',
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const meta = {
|
||||
description: 'How to change your PostgreSQL database password.',
|
||||
}
|
||||
|
||||
Your PostgreSQL database is the core of your Supabase project, so it's important that it has a strong, secure password at all times.
|
||||
Your Postgres database is the core of your Supabase project, so it's important that it has a strong, secure password at all times.
|
||||
|
||||
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres:p%3Dword@db.cvwawazfelidkloqmbma.supabase.co:5432/postgres`
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'postgres-configuration',
|
||||
title: 'Database configuration',
|
||||
slug: 'postgres-configuration',
|
||||
description: 'Updating the default congfiguration for your Postgres database.',
|
||||
subtitle: 'Updating the default congfiguration for your Postgres database.',
|
||||
}
|
||||
|
||||
Postgres provides a set of sensible defaults for you database size. In some cases, these defaults can be updated. We do not recommend changing these defaults unless you know what you're doing.
|
||||
|
||||
## Timeouts
|
||||
|
||||
By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage.
|
||||
|
||||
### Changing the default timeout
|
||||
|
||||
The timeout values were picked as a reasonable default for the majority of use-cases, but can be modified using the [`alter role`](https://www.postgresql.org/docs/current/sql-alterrole.html) statement:
|
||||
|
||||
```sql
|
||||
-- For API users
|
||||
alter role authenticator set statement_timeout = '15s';
|
||||
```
|
||||
|
||||
You can also update the statement timeout for a session:
|
||||
|
||||
```sql
|
||||
set statement_timeout to 60000; -- 1 minute in milliseconds
|
||||
```
|
||||
|
||||
### Statement Optimization
|
||||
|
||||
All Supabase projects come with the [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html) extension installed, which tracks planning and execution statistics for all statements executed against it. These statistics can be used in order to diagnose the performance of your project.
|
||||
|
||||
This data can further be used in conjunction with the [`explain`](https://www.postgresql.org/docs/current/using-explain.html) functionality of Postgres to optimize your usage.
|
||||
|
||||
## Managing Timezones
|
||||
|
||||
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
|
||||
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
|
||||
|
||||
### Change timezone
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="sql"
|
||||
>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
alter database postgres
|
||||
set timezone to 'America/New_York';
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Full list of timezones
|
||||
|
||||
Get a full list of timezones supported by your database. This will return the following columns:
|
||||
|
||||
- `name`: Time zone name
|
||||
- `abbrev`: Time zone abbreviation
|
||||
- `utc_offset`: Offset from UTC (positive means east of Greenwich)
|
||||
- `is_dst`: True if currently observing daylight savings
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="sql"
|
||||
>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
select name, abbrev, utc_offset, is_dst
|
||||
from pg_timezone_names()
|
||||
order by name;
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
### Search for a specific timezone
|
||||
|
||||
Use `ilike` (case insensitive search) to find specific timezones.
|
||||
|
||||
<Tabs
|
||||
scrollable
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="sql"
|
||||
>
|
||||
<TabPanel id="sql" label="SQL">
|
||||
|
||||
```sql
|
||||
select *
|
||||
from pg_timezone_names()
|
||||
where name ilike '%york%';
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,134 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'postgres-roles',
|
||||
title: 'Postgres Roles',
|
||||
description: 'Managing access to your Postgres database and configuring permissions.',
|
||||
subtitle: 'Managing access to your Postgres database and configuring permissions.',
|
||||
}
|
||||
|
||||
Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to confgure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security).
|
||||
|
||||
## Users vs Roles
|
||||
|
||||
In PostgreSQL, roles can function as users or groups of users. Users are roles with login privileges, while groups (also known as role groups) are roles that don't have login privileges but can be used to manage permissions for multiple users.
|
||||
|
||||
## Superuser roles
|
||||
|
||||
The superuser role has unrestricted access to the database system. Typically these roles will be able to bypass all security measures (like Row Level Security). Be cautious when granting superuser privileges as it can potentially lead to security risks.
|
||||
|
||||
## Creating roles
|
||||
|
||||
You can create a role using the `create role` command:
|
||||
|
||||
```sql
|
||||
create role "role_name";
|
||||
```
|
||||
|
||||
## Creating users
|
||||
|
||||
Roles and users are essentially the same in Postgres, however if you want to use password-logins for a specific role, then you can use `WITH LOGIN PASSWORD`:
|
||||
|
||||
```sql
|
||||
create role "role_name" with login 'extremely_secure_password';
|
||||
```
|
||||
|
||||
## Passwords
|
||||
|
||||
Your Postgres database is the core of your Supabase project, so it's important that every role has a strong, secure password at all times. Here are some tips for creating a secure password:
|
||||
|
||||
- Use a password manager to generate it.
|
||||
- Make a long password (12 characters at least).
|
||||
- Don't use any common dictionary words.
|
||||
- Use both upper and lower case characters, numbers, and special symbols.
|
||||
|
||||
### Special symbols in passwords
|
||||
|
||||
If you use special symbols in your Postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the Postgres connection string e.g. `postgresql://postgres:p%3Dword@db.xxx.supabase.co:5432/postgres`.
|
||||
|
||||
### Changing your project password
|
||||
|
||||
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to 3rd-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
|
||||
|
||||
## Granting Permissions
|
||||
|
||||
Roles can be granted various permissions on database objects using the `GRANT` command. Permissions include `SELECT`, `INSERT`, `UPDATE`, and `DELETE`. You can configure access to almost any object inside your database - including tables, views, functions, and triggers.
|
||||
|
||||
## Revoking Permissions
|
||||
|
||||
Permissions can be revoked using the `REVOKE` command:
|
||||
|
||||
```sql
|
||||
REVOKE permission_type ON object_name FROM role_name;
|
||||
```
|
||||
|
||||
## Role Hierarchy
|
||||
|
||||
Roles can be organized in a hierarchy, where one role can inherit permissions from another. This simplifies permission management, as you can define permissions at a higher level and have them automatically apply to all child roles.
|
||||
|
||||
### Role inheritance
|
||||
|
||||
To create a role hierarchy, you first need to create the parent and child roles. The child role will inherit permissions from its parent. Child roles can be added using the INHERIT option when creating the role:
|
||||
|
||||
```sql
|
||||
create role "child_role_name" inherit "parent_role_name";
|
||||
```
|
||||
|
||||
### Preventing inheritance
|
||||
|
||||
In some cases, you might want to prevent a role from having a child relationship (typically superuser roles). You can prevent inheritance relations using NOINHERIT:
|
||||
|
||||
```sql
|
||||
alter role "child_role_name" noinherit;
|
||||
```
|
||||
|
||||
## Supabase Roles
|
||||
|
||||
Postgres comes with a set of [predefined roles](https://www.postgresql.org/docs/current/predefined-roles.html). Supabase extends this with a default set of roles which are configured on your database when you start a new project:
|
||||
|
||||
### `postgres`
|
||||
|
||||
The default Postgres role. This has admin privileges.
|
||||
|
||||
### `anon`
|
||||
|
||||
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
|
||||
|
||||
### `authenticator`
|
||||
|
||||
A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then
|
||||
"change into" another role determined by the JWT verification.
|
||||
|
||||
### `authenticated`
|
||||
|
||||
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
|
||||
|
||||
### `service_role`
|
||||
|
||||
For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.
|
||||
|
||||
### `supabase_auth_admin`
|
||||
|
||||
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `auth` schema.
|
||||
|
||||
### `supabase_storage_admin`
|
||||
|
||||
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
|
||||
|
||||
### `dashboard_user`
|
||||
|
||||
For running commands via the Supabase UI.
|
||||
|
||||
### `supabase_admin`
|
||||
|
||||
Supabase Administrative role for maintaining your database.
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Postgres docs: [Database Roles](https://www.postgresql.org/docs/current/database-roles.html)
|
||||
- Official Postgres docs: [Role Membership](https://www.postgresql.org/docs/current/role-membership.html)
|
||||
- Official Postgres docs: [Function Permissions](https://www.postgresql.org/docs/current/perm-functions.html)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,380 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
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](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through 3rd 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 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 "Profiles are viewable by everyone."
|
||||
on profiles
|
||||
for select using ( auth.uid() = user_id );
|
||||
```
|
||||
|
||||
### INSERT Policies
|
||||
|
||||
You can specify select policies with the `with check` clause.
|
||||
|
||||
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)
|
||||
using ( auth.id() = user_id ); -- the actual Policy
|
||||
```
|
||||
|
||||
### UPDATE 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 only want users to be able to update 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 update their own profile."
|
||||
on profiles for update
|
||||
to authenticated -- the Postgres Role (recommended)
|
||||
using ( auth.id() = user_id ); -- the actual Policy
|
||||
```
|
||||
|
||||
### DELETE 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 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.id() = 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% | <details className="cursor-pointer">Before:<br/>No index<br/><br/>After:<br/>`user_id` indexed</details> |
|
||||
|
||||
### 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.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Admonition type="caution">
|
||||
You can only use this technique if the results of the query or function do not change based on the row data.
|
||||
</Admonition>
|
||||
|
||||
#### Benchmarks
|
||||
|
||||
| Test | Before (ms) | After (ms) | % Improvement | Change |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [test2a-wrappedSQL-uid](<https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2a-wrappedSQL-uid()>) | 179 | 9 | 94.97% | <details className="cursor-pointer">Before:<br/>`auth.uid() = user_id` <br/><br/>After:<br/> `(select auth.uid()) = user_id`</details> |
|
||||
| [test2b-wrappedSQL-isadmin](<https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2b-wrappedSQL-isadmin()>) | 11,000 | 7 | 99.94% | <details className="cursor-pointer">Before:<br/>`is_admin()` _table join_<br/><br/>After:<br/>`(select is_admin())` _table join_</details> |
|
||||
| [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% | <details className="cursor-pointer">Before:<br/>`is_admin() OR auth.uid() = user_id`<br/><br/>After:<br/>`(select is_admin()) OR (select auth.uid() = user_id)`</details> |
|
||||
| [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% | <details className="cursor-pointer">Before:<br/>`has_role() = role` <br/><br/>After:<br/>(select has_role()) = role</details> |
|
||||
| [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% | <details className="cursor-pointer">Before:<br/>`team_id=any(user_teams())` <br/><br/>After:<br/>team_id=any(array(select user_teams()))</details> |
|
||||
|
||||
### 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 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% | <details className="cursor-pointer">Before:<br/>`auth.uid() = user_id`<br/><br/>After:<br/>add `.eq` or `where` on `user_id`</details> |
|
||||
|
||||
### 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() );
|
||||
```
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Admonition type="caution">
|
||||
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)`.
|
||||
</Admonition>
|
||||
|
||||
### 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:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Admonition type="note">
|
||||
|
||||
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.
|
||||
|
||||
</Admonition>
|
||||
|
||||
#### 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% | <details className="cursor-pointer">Before:<br/>`auth.uid()` in table join on col<br/><br/>After:<br/>col in table join on `auth.uid()`</details> |
|
||||
|
||||
### 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% | <details className="cursor-pointer">Before:<br/>No `TO` policy<br/><br/>After:<br/>`TO authenticated` (anon accessing)</details> |
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -0,0 +1,122 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'postgres-triggers',
|
||||
title: 'Postgres Triggers',
|
||||
description: 'Automatically execute SQL on table events.',
|
||||
subtitle: 'Automatically execute SQL on table events.',
|
||||
}
|
||||
|
||||
In Postgres, a trigger executes a set of actions automatically on table events such as INSERTs, UPDATEs, DELETEs, or TRUNCATE operations.
|
||||
|
||||
## Creating a Trigger
|
||||
|
||||
Creating triggers involve 2 parts:
|
||||
|
||||
1. A [Function](docs/guides/database/functions) which will be executed (called the Trigger Function)
|
||||
2. The actual Trigger object, with parameters around when the trigger should be run.
|
||||
|
||||
An example of a trigger is:
|
||||
|
||||
```sql
|
||||
create trigger "trigger_name"
|
||||
after insert on "table_name"
|
||||
for each row
|
||||
execute statement trigger_function();
|
||||
```
|
||||
|
||||
## Trigger Functions
|
||||
|
||||
A trigger function is a user-defined [Function](docs/guides/database/functions) that Postgres executes when the trigger is fired.
|
||||
|
||||
### Example trigger function
|
||||
|
||||
Here is an example that updates `salary_log` whenever an employee's salary is updated:
|
||||
|
||||
```sql
|
||||
-- Example: Update salary_log when salary is updated
|
||||
create function update_salary_log()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
insert into salary_log(employee_id, old_salary, new_salary)
|
||||
values (new.id, old.salary, new.salary);
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger salary_update_trigger
|
||||
after update on employees
|
||||
for each row
|
||||
exectute function update_salary_log();
|
||||
```
|
||||
|
||||
### Trigger variables
|
||||
|
||||
Trigger functions have access to several special variables that provide information about the context of the trigger event and the data being modified. In the example above you can see the values inserted into the salary log are `old.salary` and `new.salary` - in this case `old` specifies the previous values and `new` specifies the updated values.
|
||||
|
||||
Here are some of the key variables and options available within trigger functions:
|
||||
|
||||
- `TG_NAME`: The name of the trigger being fired.
|
||||
- `TG_WHEN`: The timing of the trigger event (`BEFORE` or `AFTER`).
|
||||
- `TG_OP`: The operation that triggered the event (`INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`).
|
||||
- `OLD`: A record variable holding the old row's data in `UPDATE` and `DELETE` triggers.
|
||||
- `NEW`: A record variable holding the new row's data in `UPDATE` and `INSERT` triggers.
|
||||
- `TG_LEVEL`: The trigger level (`ROW` or `STATEMENT`), indicating whether the trigger is row-level or statement-level.
|
||||
- `TG_RELID`: The object ID of the table on which the trigger is being fired.
|
||||
- `TG_TABLE_NAME`: The name of the table on which the trigger is being fired.
|
||||
- `TG_TABLE_SCHEMA`: The schema of the table on which the trigger is being fired.
|
||||
- `TG_ARGV`: An array of string arguments provided when creating the trigger.
|
||||
- `TG_NARGS`: The number of arguments in the `TG_ARGV` array.
|
||||
|
||||
## Types of Triggers
|
||||
|
||||
There are two types of trigger, `BEFORE` and `AFTER`:
|
||||
|
||||
### Trigger before changes are made
|
||||
|
||||
Executes before the triggering event.
|
||||
|
||||
```sql
|
||||
create trigger before_insert_trigger
|
||||
before insert on orders
|
||||
for each row
|
||||
execute statement before_insert_function();
|
||||
```
|
||||
|
||||
### Trigger after changed are made
|
||||
|
||||
Executes after the triggering event.
|
||||
|
||||
```sql
|
||||
create trigger after_delete_trigger
|
||||
after delete on customers
|
||||
for each row
|
||||
execute statement after_delete_function();
|
||||
```
|
||||
|
||||
## Execution frequency
|
||||
|
||||
There are two options available for executing triggers:
|
||||
|
||||
- `for each row`: specifies that the trigger function should be executed once for each affected row.
|
||||
- `for each statement`: the trigger is executed once for the entire operation (eg, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
|
||||
|
||||
## Dropping a Trigger
|
||||
|
||||
You can delete a trigger using the `drop trigger` command:
|
||||
|
||||
```sql
|
||||
drop trigger "trigger_name" on "table_name";
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- Official Postgres Docs: [Triggers](https://www.postgresql.org/docs/current/triggers.html)
|
||||
- Official Postgres Docs: [Overview of Trigger Behavior](https://www.postgresql.org/docs/current/trigger-definition.html)
|
||||
- Official Postgres Docs: [CREATE TRIGGER](https://www.postgresql.org/docs/current/sql-createtrigger.html)
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -2,12 +2,12 @@ import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
title: 'Query Optimization',
|
||||
description: 'Choosing indexes',
|
||||
description: 'Choosing indexes to improve your query performance.',
|
||||
subtitle: 'Choosing indexes to improve your query performance.',
|
||||
footerHelpType: 'postgres',
|
||||
}
|
||||
|
||||
When working with PostgreSQL, or any relational database, indexing is key to improving query performance.
|
||||
Aligning indexes with common query patterns can speed up data retrieval by an order of magnitude.
|
||||
When working with Postgres, or any relational database, indexing is key to improving query performance. Aligning indexes with common query patterns can speed up data retrieval by an order of magnitude.
|
||||
|
||||
This guide is intended to:
|
||||
|
||||
@@ -90,7 +90,7 @@ explain select * from customers where sign_up_date > 25;
|
||||
|
||||
### Use Appropriate Index Types
|
||||
|
||||
PostgreSQL offers various index types like [B-tree, Hash, GIN, etc](https://www.postgresql.org/docs/current/indexes-types.html). Select the type that best suits your data and query pattern. Using the right index type can make a significant difference. For example, using a BRIN index on a field that always increases and lives within a table that updates infrequently - like `created_at` on an `orders` table - routinely results in indexes that are +10x smaller than the equivalent default B-tree index. That translates into better scalability.
|
||||
Postgres offers various index types like [B-tree, Hash, GIN, etc](https://www.Postgres.org/docs/current/indexes-types.html). Select the type that best suits your data and query pattern. Using the right index type can make a significant difference. For example, using a BRIN index on a field that always increases and lives within a table that updates infrequently - like `created_at` on an `orders` table - routinely results in indexes that are +10x smaller than the equivalent default B-tree index. That translates into better scalability.
|
||||
|
||||
```sql
|
||||
create index idx_orders_created_at ON customers using brin(created_at);
|
||||
@@ -127,7 +127,7 @@ analyze customers;
|
||||
|
||||
---
|
||||
|
||||
By following this guide, you'll be able to discern where indexes can optimize queries and enhance your PostgreSQL performance. Remember that each database is unique, so always consider the specific context and use case of your queries.
|
||||
By following this guide, you'll be able to discern where indexes can optimize queries and enhance your Postgres performance. Remember that each database is unique, so always consider the specific context and use case of your queries.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import Layout from '~/layouts/DefaultGuideLayout'
|
||||
|
||||
export const meta = {
|
||||
id: 'timeouts',
|
||||
title: 'Timeouts',
|
||||
description: 'Timeouts and optimization',
|
||||
}
|
||||
|
||||
By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage.
|
||||
|
||||
### Changing the default timeout
|
||||
|
||||
The timeout values were picked as a reasonable default for the majority of use-cases, but can be modified using the [`alter role`](https://www.postgresql.org/docs/current/sql-alterrole.html) statement:
|
||||
|
||||
```sql
|
||||
-- For API users
|
||||
alter role authenticator set statement_timeout = '15s';
|
||||
```
|
||||
|
||||
You can also update the statement timeout for a session:
|
||||
|
||||
```sql
|
||||
set statement_timeout to 60000; -- 1 minute in milliseconds
|
||||
```
|
||||
|
||||
### Statement Optimization
|
||||
|
||||
All Supabase projects come with the [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html) extension installed, which tracks planning and execution statistics for all statements executed against it. These statistics can be used in order to diagnose the performance of your project.
|
||||
|
||||
This data can further be used in conjunction with the [`explain`](https://www.postgresql.org/docs/current/using-explain.html) functionality of Postgres to optimize your usage.
|
||||
|
||||
export const Page = ({ children }) => <Layout meta={meta} children={children} />
|
||||
|
||||
export default Page
|
||||
@@ -4,24 +4,21 @@ export const meta = {
|
||||
id: 'webhooks',
|
||||
title: 'Database Webhooks',
|
||||
description: 'Trigger external payloads on database events.',
|
||||
video: 'https://www.youtube.com/v/codAs9-NeHM',
|
||||
subtitle: 'Trigger external payloads on database events.',
|
||||
tocVideo: 'codAs9-NeHM',
|
||||
}
|
||||
|
||||
Database Webhooks allow you to send real-time data from your database to another system whenever a table event occurs.
|
||||
|
||||
You can hook into three table events: `INSERT`, `UPDATE`, and `DELETE`. All events are fired _after_ a database row is changed.
|
||||
|
||||
## Webhooks vs Triggers
|
||||
|
||||
Database Webhooks are very similar to triggers, and that's because Database Webhooks are just a convenience wrapper around triggers
|
||||
using the [pg_net](/docs/guides/database/extensions/pgnet) extension. This extension is asynchronous, and therefore will not block your database changes for long-running network requests.
|
||||
|
||||
This video demonstrates how you can create a new customer in Stripe each time a row is inserted into a `profiles` table:
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
Database Webhooks were previously known as Function Hooks.
|
||||
|
||||
</Admonition>
|
||||
|
||||
<div className="video-container">
|
||||
<iframe
|
||||
src="https://www.youtube-nocookie.com/embed/codAs9-NeHM"
|
||||
|
||||
@@ -138,46 +138,7 @@ For working with JWT and Auth functions.
|
||||
|
||||
### Roles
|
||||
|
||||
Supabase creates several default roles in your Postgres database. To restore defaults at any time you can run the commands inside the
|
||||
[schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your
|
||||
[role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments.
|
||||
|
||||
##### `postgres`
|
||||
|
||||
The default PostgreSQL role. This has admin privileges.
|
||||
|
||||
##### `anon`
|
||||
|
||||
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
|
||||
|
||||
##### `authenticator`
|
||||
|
||||
A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then
|
||||
"change into" another role determined by the JWT verification.
|
||||
|
||||
##### `authenticated`
|
||||
|
||||
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
|
||||
|
||||
##### `service_role`
|
||||
|
||||
For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.
|
||||
|
||||
##### `supabase_auth_admin`
|
||||
|
||||
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `auth` schema.
|
||||
|
||||
##### `supabase_storage_admin`
|
||||
|
||||
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
|
||||
|
||||
##### `dashboard_user`
|
||||
|
||||
For running commands via the Supabase UI.
|
||||
|
||||
##### `supabase_admin`
|
||||
|
||||
Supabase Administrative role for maintaining your database.
|
||||
Supabase creates several [default roles](/docs/guides/database/postgres-roles) in your Postgres database. To restore defaults at any time you can run the commands inside the [schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your [role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments.
|
||||
|
||||
### Realtime Logs
|
||||
|
||||
|
||||
@@ -2273,4 +2273,19 @@ module.exports = [
|
||||
destination: '/docs/guides/platform/oauth-apps/build-a-supabase-integration',
|
||||
},
|
||||
{ permanent: true, source: '/docs/reference/cli/config', destination: '/docs/guides/cli/config' },
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/database/timeouts',
|
||||
destination: '/docs/guides/database/postgres/configuration',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/database/managing-timezones',
|
||||
destination: '/docs/guides/database/postgres/configuration',
|
||||
},
|
||||
{
|
||||
permanent: true,
|
||||
source: '/docs/guides/database/managing-passwords',
|
||||
destination: '/docs/guides/database/postgres/roles#passwords',
|
||||
},
|
||||
]
|
||||
Reference in new issue
Block a user