Migrate to RunxBuild today!! Deploy More while Spending Less

Calculate your savings
unxBuild
Back to Blog Explainer

Supabase Postgres: It Is Plain Postgres, and That Changes How You Should Use It

Sean

Platform Writer

Sep 21, 2026
9 min read

Supabase Postgres is a real, unmodified PostgreSQL database with a handful of extensions installed, and everything else the product offers, the REST API, the auth system, the realtime feed, the storage service, is a separate process that talks to that database. This matters in both directions. It means you can connect with psql, run migrations with any tool, use every Postgres feature, and take a pg_dump and walk away. It also means the auto-generated API is only as safe as the row level security policies you write, because the API is a thin translation of HTTP into SQL and Postgres is doing the authorisation. Treat it as Postgres first and the rest makes sense.

Supabase Postgres: It Is Plain Postgres, and That Changes How You Should Use It

Most confusion about the product comes from treating the dashboard as the database. The dashboard is a client. The database is Postgres, it behaves exactly like Postgres, and the decisions that matter, connection mode, security policies, and where the application logic lives, are Postgres decisions with a Supabase-specific default.

Table of contents

What is actually running

A project is one Postgres instance plus a set of services that each connect to it as ordinary database clients.

  • PostgREST turns the schema into a REST API. A GET /rest/v1/orders?status=eq.paid becomes a SELECT with a WHERE clause, executed as a database role derived from the caller’s JWT.
  • GoTrue handles sign-up, sign-in and tokens, and stores users in an auth schema inside the same database.
  • Realtime listens to the write-ahead log and broadcasts inserts, updates and deletes over websockets.
  • Storage keeps file metadata in a storage schema and the bytes in an object store.
  • Edge Functions are a separate runtime that, like everything else, connects to the database when it needs data.

None of those services owns the data. Delete every one of them and the tables, rows, indexes, functions and policies are still there in a Postgres you could point any other tool at. That is the property that makes the platform worth building on, and it is also why the shortcuts it offers have to be understood as shortcuts over SQL rather than as a different system.

The three connection strings and when each is right

A project exposes the database on a direct connection and through a connection pooler, and the pooler has two modes. Picking the wrong one is the most common source of production trouble.

  1. Direct connection, port 5432. A plain TCP connection to Postgres. Every feature works: prepared statements, LISTEN/NOTIFY, long transactions, session-level settings. Each connection is a real backend process, and the instance has a hard limit on how many it can hold. Right for a long-running server with its own pool, migrations, and admin tools.
  2. Pooler in transaction mode, port 6543. A PgBouncer-style proxy that hands a backend to a client only for the duration of a transaction. Thousands of short-lived clients can share a few dozen backends. Prepared statements and session settings do not survive between transactions, so ORMs must be told to disable them. Right for serverless functions, edge runtimes, and anything that opens a connection per request.
  3. Pooler in session mode. The proxy keeps a backend for the whole client session. Compatible with everything, but the multiplexing benefit mostly disappears. Use it when transaction mode breaks a feature you need and you still want the pooler in front.
# long-running server, own pool
postgresql://postgres.[ref]:[password]@db.[ref].supabase.co:5432/postgres

# serverless / per-request clients
postgresql://postgres.[ref]:[password]@[region].pooler.supabase.com:6543/postgres?pgbouncer=true

The failure that follows from getting this wrong is a wall of too many connections errors from serverless functions on a direct connection, or prepared statement does not exist from an ORM on a transaction-mode pooler. Both are fixed by changing the string, not the code.

Row level security is the whole security model

When a browser calls the REST API with the public anon key, PostgREST executes the query as the anon role. When it calls with a signed-in user’s JWT, it executes as authenticated with the user’s id available to SQL. There is no application server in between deciding what that user may see. The database decides, using row level security policies on each table.

This is elegant and it is unforgiving. A table with RLS disabled and the anon key in a public JavaScript bundle is a table anyone on the internet can read and write. The correct default is RLS on for every table in the public schema, with no policies until you write them, which means the API returns nothing until you decide what it should return.

alter table orders enable row level security;

create policy "owners read their orders"
  on orders for select
  to authenticated
  using (user_id = auth.uid());

create policy "owners insert their orders"
  on orders for insert
  to authenticated
  with check (user_id = auth.uid());

Policies are Postgres objects. They live in migrations, they show up in pg_dump, and they apply to every client, including your own server-side code unless it connects with a role that bypasses them. That last point is the escape hatch: the service role key skips RLS and belongs only on a server you control, never in a client.

Where the application logic should live

The platform makes it easy to put logic in three places and offers little guidance on which. A workable split:

  • In the database for invariants: constraints, triggers that maintain derived columns, functions exposed as RPC when a client needs a multi-step operation to be atomic. Postgres is good at this and it applies regardless of which client made the call.
  • In an Edge Function for short request handlers that need a secret the browser cannot hold: calling a payment provider, sending an email, validating a webhook signature.
  • In an always-on service for everything that runs long, runs on a schedule, holds connections open, or would be painful to debug inside a function runtime. A Node or Python process with a direct connection and its own pool, deployed from a repository, is the boring answer and the one most projects end up at.

The anti-pattern is building the entire backend as browser-to-database calls with the logic in the frontend and RLS as the only guard. It works for a prototype and becomes unreviewable at the moment the business rules get complicated, because the rules are scattered across React components and policies with no single place to read them.

Leaving, and why the exit is the selling point

Because the database is unmodified Postgres, the migration out is a pg_dump and a pg_restore into any other Postgres. The auth and storage schemas come with it. Row level security policies, functions and triggers come with it. What does not come with it is the services: you would replace PostgREST with your own API, GoTrue with another auth provider or your own tables, and Realtime with a change-data-capture setup or polling.

That is a fair trade and a rare one. Most backend platforms hold the data in a proprietary shape that makes leaving a rewrite. Here the data is portable and the convenience layer is what you would give up, which is the right way around. It also means the decision to self-host, move to a plain managed Postgres, or run the same schema beside a conventional API service can be made later, on evidence, rather than up front.

How this fits the rest of the stack

The point where a project moves from the free tier to something it pays for is usually the point where it also needs a proper service beside the database: a worker, a scheduled job, an API that holds a connection pool. Modelling both together is the honest comparison, and the RunxBuild hosting calculator does that with a managed Postgres instance next to a service plan, connection limits stated per plan, and private networking between the two included. Because the schema is plain Postgres, the same migrations and the same policies run there unchanged.

Useful related references:

FAQ

Is Supabase Postgres real PostgreSQL?

Yes. It is unmodified Postgres with extensions such as pgvector, pg_graphql and pgsodium installed. Any Postgres client, migration tool or backup tool works against it, and a pg_dump restores into any other Postgres instance.

Should I use the direct connection or the pooler?

Use the direct connection on port 5432 for long-running servers with their own pool and for migrations. Use the pooler in transaction mode on port 6543 for serverless functions and per-request clients, and disable prepared statements in your ORM when you do.

What happens if I forget to enable row level security?

Any client holding the public anon key, which is every visitor to your site, can read and write the whole table through the REST API. Enable RLS on every table in the public schema and add explicit policies; with RLS on and no policies, the API returns nothing, which is the safe default.

Where should I put backend logic on Supabase?

Invariants and atomic multi-step operations in the database as constraints, triggers and functions. Short handlers that need secrets in Edge Functions. Long-running, scheduled or connection-heavy work in an always-on service with a direct connection and its own pool.

How hard is it to migrate away from Supabase?

The data is easy: pg_dump and restore into any Postgres, including policies, functions and the auth and storage schemas. The services are the work: you would replace the auto-generated REST API, the auth service and the realtime feed with your own equivalents.

#supabase postgres#supabase database#supabase connection pooler#supabase vs postgres#supabase row level security