RLS Policies
To protect your data, you can apply row-level-security policies to your data. This article lists some common policies you can apply to your data in NextKit.
Supabase uses RLS policies to protect your data. RLS are useful for two main reasons:
- Supabase exposes your database directly to your clients. This means that your clients can query your database directly. RLS policies allow you to restrict what data they can access.
- You can use SQL policies to apply access rules to your data. This helps you to keep your data secure without having to write any additional code.
In this document, we'll list some common RLS policies you can apply to your data in NextKit.
You can copy and paste some of this (provided the table names match) into your database so that you can get started quickly.
Common RLS Policies
Restricting access to a table based on a column
This policy will restrict access to a table based on a column. In this example, we're restricting access to the users
table based on the id
column.
Restricting write access to a table based on a column
This policy will restrict write access to a table based on a column. In this example, we're restricting write access to the users
table based on the id
column.
Truthfully, this is not a very useful policy. It's unlikely that you'll want to restrict access to a table based on the user ID in a multi-tenant application. However, it's a good example if you store per-user data in a table.
Restrictive Policies
If you want these rules to be restrictive
(i.e. they apply to all queries), then you can add as restrictive
:
This means that if you have 2 select
policies, one restrictive
and one permissive
, then the restrictive
policy will take precedence. If the restrictive
policy fails, then the permissive
policy will not apply.
This is useful when a policy needs to apply to all queries.
Restricting access to a table based on a column and a role
In NextKit - multi-tenancy is achieved using organizations
. As such, you'll want to restrict access to data based on the organization_id
column.
Let's create a policy that restricts access to the tasks
table based on the organization_id
column - taking into consideration the user's role in the organization.
User roles are defined using a hierarchy. The hierarchy is as follows:
- Owner (role = 2)
- Admin (role = 1)
- Member (role = 0)
You can update this as you see fit - but this is the default hierarchy. Remember that the role
column is stored in the memberships
table and needs to be updates should you wish to change the hierarchy.
Create the function to get the user's role
Create the policies
Now that we have the function to get the user's role, we can create the policies. The policy state that only the owner can insert, update, or delete tasks.
Let's now allow every organization member to read tasks.
We can use the built-in current_user_is_member_of_organization
function to check if the user is a member of the organization.
NB: when using a select
policy, you don't need to use with check
. This is because the using
clause is sufficient to restrict access to the data. With that said, remember than failing policies won't throw an error - they'll just return no data.
We can also allow every member to create tasks within their organization using the insert
policy:
Subscriptions and RLS
RLS policies are applied to subscriptions. This means that if you have a subscription that returns data that the user doesn't have access to - then the subscription will fail.
Let's see how we can use RLS for restricting access to subscriptions.
This is very dependent on your use case. You'll need to think about how you want to restrict access to your data. But for the sake of this example, let's assume that we want to restrict how many tasks a user can write to the database.
Storing your Plans in the database
First, we'll need to store our plans in the database. We'll create a plans
table that stores the id
, name
, and max_tasks
for each plan.
You have two options here when talking about Stripe:
- assign a plan to a Product ID
- assign a plan to a Price ID
If you need different quotas for monthly and yearly plans - then you'll need to assign a plan to a Price ID. If you don't need different quotas for monthly and yearly plans - then you can assign a plan to a Product ID.
In the example below, we're assigning a plan to a Price ID.
We also allow read access to this table for all authenticated users:
A function to get the organization's subscription
Let's create an SQL function that returns the organization's subscription:
A function to get the organization's task count
Let's create an SQL function that returns the organization's task count:
A function to check if the organization can create a task
Let's create an SQL function that returns true
if the organization can create a task:
Here we store the total tasks count in a variable:
Then we get the organization's price ID:
If the organization doesn't have an active subscription - then we throw an error:
What if you wanted to provide a default plan for organizations that don't have an active subscription? You could do something like this for allowing 10 tasks for organizations that don't have an active subscription:
Then we get the plan's task quota:
Finally, we check that the count of tasks is less than the plan's task quota:
Wrapping it all up in a policy
We can now write a policy that restricts access to the tasks
table based on the organization's task quota:
You don't always need RLS
Sometimes - you don't need RLS. As long as RLS is enabled on the table - you can think about not applying any RLS policies.
Instead - you can manage access to the data using traditional server-side code. This is useful if you want to apply more complex access rules to your data or when the data is not directly updated by the client - but instead by a server-side process (such as a cron job or a webhook).
Restricting Functions and Stored Procedures
Sometimes - you need a security definer function or stored procedure. This is useful for bypassing RLS policies. But it's risky.
In Makerit, we added a function to restrict calling these functions only from the server using a service role key - so that these functions can't be called from the client - and allowing you to perform the required checks before calling the function.
The function is assert_service_role
. You can perform a check like this:
This function will throw an error if it's called from the client. You can then call this function from the server using a service role key and ensure that the user has enough quota to create a task server-side before calling this function.