Knowledge baseAugust 7, 2026·updated August 7, 2026

Supabase: database, authorization and API without its own backend

Managed PostgreSQL, automatic REST on top of the schema, login by mail and OAuth, and rights are set by line-by-line policies right in the database. Analysis of the mechanics, traps of RLS and ways to verify that the data is really closed.

What does this solve?

Any product with a personal account comes up against the same set of tasks even before the first line of business logic: where to store the data, how to create users, who has the right to read someone else’s line, where the frontend will get the JSON from. Supabase closes the entire set at once - managed PostgreSQL, automatic HTTP interface on top of the schema, registration and login service, file storage, subscription to string changes. A layer that only transfers data from the database to the browser ceases to be necessary. Weeks are saved on password recovery, login buttons through external providers, handwritten CRUD endpoints and access control.

How it works

The foundation is a regular PostgreSQL database, which you can connect to via psql and view the tables. Everything else is hung on top by separate services:

  • PostgREST reads the database schema and sends the HTTP interface. Table orders turns into /rest/v1/orders, filters are written in the query line (?status=eq.paid&select=id,total), the function in the database is called via /rest/v1/rpc/имя_функции. The new column appears in the API immediately after migration.
  • Authentication service keeps users in the schema auth and issues JWT. Able to provide a password, email link, one-time code, and login through external providers.
  • Realtime listens to the Postgres replication log and pushes row changes to the WebSocket. There are also broadcast channels and presence for “who is on the page now”.
  • Storage stores files, and the rights to them are described by the same SQL policies as the rights to table rows.
  • Edge Functions - server code on Deno for things that cannot be trusted in the browser: receiving a webhook from a payment system, going to an external API with a secret key, sending a letter.
  • Studio and CLI — web interface to the database and local stack in Docker for development.

The request path looks like this. The browser supplies two headers: the project's public key (anon) and Authorization: Bearer <JWT пользователя>. PostgREST parses token, switches connection to role authenticated, puts the contents of the token into the session settings and translates the query string into SQL. Next, Row Level Security comes into play—line-by-line policies within the database itself:

alter table orders enable row level security;

create policy "свои заказы видно"
  on orders for select
  using (auth.uid() = user_id);

create policy "свои заказы можно менять"
  on orders for update
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

auth.uid() retrieves the user ID from the token, auth.jwt() —all other contents, including role and custom metadata. using controls which rows are visible to the operation, with check —which lines are allowed to be written. While the table's RLS is turned off, its contents are available to the outside world to anyone who took the public key from the front-end source. Enabled without policies - the table responds with emptiness. There is no third state.

Access check lives in the database, not in the application code - a new table without a policy is open to the entire Internet.

There are two keys, and they should not be confused. anon is browser-based and operates under RLS restrictions. service_role bypasses RLS completely; it belongs in the environment variables of the server process and nowhere else - neither in the mobile application, nor in the client bundle, nor in the repository.

The scheme is deployed by migrations. supabase init creates the project structure, supabase start lifts the entire stack locally in containers, supabase db diff collects an SQL file from accumulated changes, supabase db push rolls it onto the cloud project. Postgres extensions are installed normally create extension — pgvector for embeddings, pg_cron for schedules, PostGIS for geometry.

Useful scripts

  • MVP with personal account: registration, login via email and external provider, user tables, access via RLS - without a single server route.
  • Internal admin panel or dashboard for the team: visibility is limited by policies based on the role from the token, the interface is assembled on any front-end framework.
  • Mobile application: client libraries for major platforms use the same REST as the web.
  • Live counters and co-editing: subscribing to table changes via Realtime instead of polling the server using a timer.
  • Database for searching documents: pgvector next to business tables, embeddings and metadata in one place, rights to documents - the same policies.
  • Receiving webhooks from payments and external systems: Edge Function verifies the signature and writes to the database under service_role.

Limitations

RLS policies remain the only barrier between the public key and the data. A forgotten policy on a new table reveals it to everyone, and not a single linter will warn you about it. Each table has to be closed by hand, and a set of policies has to be tested in the same way as code.

Policies are executed on each row of the sample. Subquery in using, which goes to another table, easily turns the usual one select in a difficult operation. It can be cured by moving the check to a function with security definer and the index for it, but you will have to remember this constantly.

Complex business logic does not fit into the browser. Price calculations, status transitions, everything that cannot be shown to the client goes into database functions or Edge Functions - and the project code spreads across three environments: SQL, Deno, frontend. Debugging such sprawl costs more than a single monolithic server.

The built-in connection pooler in transactional mode does not work well with prepared expressions and session settings. An ORM designed for constant connection behaves unpredictably; For such clients, a separate puller mode is needed.

Realtime on a replication log is not cheap: a bulk table update generates a stream of events, and filtering occurs after delivery. The file storage does not provide transformations at the level of specialized CDNs, except for basic image resizing.

The free tariff suspends projects without activity, and quotas for database volume and traffic hit the ceiling earlier than it seems from the demo. Self-hosting is possible: this is a docker-compose for a dozen services, which you will have to update, monitor and backup on your own.

Moving from Supabase is cheaper than from closed platforms - the data remains in Postgres, the dump is transferred anywhere. But authorization, policies and Edge Functions are tied to a specific implementation and are rewritten anew.

How to check the result

The first step is to try to steal data with your own public key. Take anon From the frontend and without any JWT, knock on each table:

curl "https://<проект>.supabase.co/rest/v1/orders?select=*" \
  -H "apikey: <anon>"

Empty array - the policy is working. Rows in the response—the table is open. Then do the same with the test user token: two accounts are created, each with its own entry, each should see exactly one.

The list of tables without RLS is displayed by querying the system catalog:

select relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
  and c.relkind = 'r'
  and not c.relrowsecurity;

The result must be empty. This request should be checked before each layout - it catches a forgotten table before it is found outside.

Further performance policies: explain analyze on heavy sampling on behalf of the role authenticated will show what the condition turned into. Sequential scanning instead of index scanning means that the expression in using is non-indexable and will hit a timeout as the table grows.

Secrets are checked by searching the collected bundle: line service_role should not appear in any file going to the browser. Login is verified by a full pass on a real mailbox - registration, letter, confirmation, logout, password recovery, re-entry. The built-in email sender is only suitable for development; in production, your own SMTP is connected, and this is a separate checklist item before launch.

Local stack (supabase start) must rise from scratch and roll all migrations onto a clean database without a single error. As soon as this stops working, the scheme in the cloud has diverged from the migrations in the repository, and the next release will reveal a discrepancy already in production.

  • supabase
  • postgresql
  • backend
  • authorization