In today's data-driven world, securing your applications and databases is paramount. Supabase, a powerful open-source platform offering a backend-as-a-service (BaaS) solution, empowers developers to build secure and scalable applications. While Supabase offers robust built-in functionalities, understanding and implementing best practices during project setup is crucial to ensure a robust security posture. This blog post will guide you through key considerations and best practices to establish a secure foundation for your Supabase project.

Need help with making your Supabase project more secure? Book a free 30-minute consultation with our Supabase expert.

Authentication

Once you set up your project, the most basic security decision you must make is to use authentication.

Password Complexity

If you decide to include the traditional password as an authentication method, make sure that you elevate the default password security policy.

Set the minimum length to 8 characters. We (and OWASP.org) recommend 12 characters, but this decision must balance security and user convenience.

Set password complexity to the recommended “Lowercase, uppercase, digits, and symbols” approach.

Enable the prevention of leaked passwords, which Supabase provides. It uses the API of the HaveIBeenPawned database.

You might want to consider enabling the “Captcha protection” for additional protection from bots and brute force attacks.

Ensure you have enabled the “Detect and revoke potentially compromised refresh tokens” option.

For more details: https://supabase.com/docs/guides/auth/passwords

Building Your Supabase Project: Start with Secure Foundations

Passwordless Methods

Consider enabling passwordless methods, which allow the hassle of user management to be avoided on the part of the user.

For more details: https://supabase.com/docs/guides/auth/passwordless-login

MFA - Multifactor Authentication

If your application has high-security needs or you have roles that manage sensitive information, you should enable MFA. 

You can then restrict these users from accessing specific data if they have not set up their MFA method. Here’s more on how to apply these restrictions on RLS.

RBAC - Role-Based Access Control

Scarcely, there are projects where all users have the same access level to data. Most likely, your project will need the “user” (normal user) role and an “admin” role (even if the admin is a single account).

So, better prepare and plan your roles. You can create, for example, a roles table with permissions, etc, and assign users to roles. This will allow assigning/unassigning roles and updating roles easier.

You can, in addition, create an Auth Hook, that is run before an authentication token is issued and allows you to modify the token. So you can insert the role of a user in the JWT (authentication) token.

This allows you to add RLS (see below) rules that restrict access to specific data to specific roles or similar role-based policies.

For more details: https://supabase.com/docs/guides/auth/custom-claims-and-role-based-access-control-rbac

RLS - Row Level Security

Database tables in the public schema are accessible by default via the Supabase Data API. You don’t want that. So you need to enable RLS and then create policies to govern what is allowed to go in and out of the table rows.

Any table in public with RLS disabled is accessible by the anon role (effectively by any non-authenticated user) via the PostgreREST API and the clients.

Warning: Make sure that Row Level Security is enabled on all public tables. If you create a table via the Dashboard, RLS is enabled by default. But if it was created in another way (migration scripts, SQL console, etc.), RLS will not be enabled by default.

Warning: Make sure that Row Level Security is enabled on all public tables. If you create a table via the Dashboard, RLS is enabled by default. But if it was created in another way (migration scripts, SQL console, etc.), RLS will not be enabled by default.

Then, create RLS policies to govern and restrict access to a table on the row level. In a policy, you define the rules that dictate whether an action (SELECT, INSERT, UPDATE, etc.) is allowed for a specific row.

For example, a basic RLS policy allows an authenticated user to SELECT only her rows, is:

RLS policies are like additional WHERE clauses automatically applied to row actions.


Read: "What’s New in Expo SDK 50: New Features and Updates."

RBAC Policies

Coming back to the RBAC policies mentioned above, RLS allows you to apply your RBAC by accessing the authentication data that are available in every request to the Supabase. You may access the authentication information with the helper functions auth.uid() and auth.jwt(). Based on this you can apply restriction rules.

You can find more details about RLS in the official documentation:

https://supabase.com/docs/guides/auth/row-level-security

MFA Policy Enforcement

The JWT token can also obtain information about the multifactor authentication of the user (remember the custom claims mentioned above) that is requesting the action. You can apply the policies that demand that a user or role has MFA enabled to perform specific tasks on specific tables:

For more details: https://supabase.com/docs/guides/auth/row-level-security#using-multi-factor-authentication

Block Direct Access to Tables, Views, Functions

By default, every table is accessible via the Supabase REST API. You might want to avoid this and allow access to specific tables only via Postgres Functions. This will increase complexity as you will have to develop and maintain more functions. On the other hand, allowing access only via functions gives a more fine-grained control of enforcing these policies.

Such policies can be achieved using the pgrst.db_pre_request command. This allows the execution of a function before each API request is served.

So before each API request, you can decide whether to allow it or not (that is happening before RLS).

You can access information about the request by using the current_settings() function. This will return request data like the method (GET, POST, etc), the target table, view, function, cookies, jwt, etc.

Other API  request policies

You can use the db_pre_request command to implement other protecting measures, like:

  • Enforce rate limit per IP
  • Check custom API keys
  • Apply request quotas

And many more.

Find more details here: https://supabase.com/docs/guides/api/securing-your-api#enforce-additional-rules-on-each-request

SafeUpate

Make sure that you have the safeupdate Postgres extension enabled. It is enabled by default for all API queries, but you should make sure that it is enabled.

This extension ensures that any delete() or update() request comes with a WHERE clause or a filtering that limits the action to specific fields. You can’t accidentally just call DELETE FROM <table_name> and have this command pass through.

You can find detailed information on how to check this here:
https://supabase.com/docs/guides/api/securing-your-api#safeguards-towards-accidental-deletes-and-updates

Security Definer Check

If you use RLS policies you will have at some point to create security definer functions. 

These are functions that run not with the caller privileges but with the “creator” privileges.

So, they can be very powerful, and they should only be used by RLS policies or other functions and not directly exposed.

So, that is why they should never be defined in a schema that is exposed to the API. By default, “exposed” schemas are the public, storage, and grapql_public.

You should check your API settings to see which schemas are exposed, and make sure that you don’t have security definer functions in there.

Read "What is Expo Router?"

Client Side Checks

The following security checks apply to the client side that consumes your database. That is, web applications, mobile applications, and whatever sits in the user’s devices, which is considered an unsafe environment.

Don’t expose “Service” Keys

Supabase comes with a “Service Role” API key that allows you to bypass RLS policies. It should only be used on the server side for special work.

The “Service Role” key should never be exposed to the client environment. 

So check your client-side code and .env files to ensure there are no such keys. Remember that web apps and mobile apps can’t hold secrets as the code is exposed to the user environment.

Service Roles keys can be found in your Supabase Dashboard / Settings / API settings.

Read: "Building an offline-first app with Expo, Supabase and WatermelonDB".

Use Supabase Clients

Don’t set up a direct connection to your Supabse database from the client. Always use the official client libraries or the PostgREST API to access the database.

This is because all the inputs to the database are “parametrised,” and the statements are “prepared.” Thus, there is no risk of an SQL Injection attack this way.

If you choose to allow direct access to the PostgreSQL database, then you should take extra care of SQL injection attacks.

You’re Done (?)

No way you are done with security. Security is not only secure coding but also processes, reviews, and design of your project and database. 

However, considering and following some of the above practices will get you to a solid start.

Happy Coding!

Need help with making your Supabase project more secure? Book a free 30-minute consultation with our Supabase expert.
More insights