Last updated: 6 November 2026

Supabase vs Postgres is a strange matchup, because Supabase is Postgres. What you're really deciding is whether to pay for the pieces Supabase puts around it: sign-in, file storage, live updates, an auto-generated API and backups. For some apps that bundle is the best $25 you'll spend. For others it's $25 a month for a database plus features you never call. This guide shows what each piece costs on its own, where Supabase clearly wins, and how to keep your Supabase-style security rules working on plain Postgres if you move.

Try it on Hubfly space: a managed PostgreSQL 18 database starts at about $2 a month.

Prices are as of 6 November 2026, in US dollars.

What Supabase adds on top of Postgres

Every Supabase project is a regular Postgres database. Around it, Supabase runs several services and gives them one dashboard, one client library and one bill.

Supabase piece What it does for you Plain-Postgres replacement
Auth Sign-up, login, OAuth, magic links; puts the user ID in every request An auth library in your API (e.g. Better Auth or Auth.js), or a hosted service like Clerk
Auto-generated API Query tables straight from the browser, guarded by row-level security Your own small API (Express, FastAPI, Go…)
Row-level security helpers auth.uid() inside policies The same policies, plus a 10-line shim (below)
Storage Files in buckets with access rules Any S3-compatible bucket
Realtime Live updates over WebSockets A Pusher-compatible server, or Postgres LISTEN/NOTIFY in your API
Edge Functions Server code next to the database Code in your API container
Backups Daily backups on paid plans, optional point-in-time restore Managed backups from your provider, or scheduled pg_dump

The pattern is clear: Supabase's extras replace code you would otherwise write and run. That's the trade. You pay less for plain Postgres, and you write (or wire up) more yourself.

Supabase pricing in 2026

From Supabase's pricing page:

  • Free: $0. 500 MB database, 50,000 monthly active users, 1 GB of files, 5 GB of egress, 2 active projects. No automatic backups. Projects are paused after 1 week of inactivity.
  • Pro: from $25/month. Includes $10 of compute credit, which covers one Micro instance (shared CPU, 1 GB RAM). 8 GB of disk, 100,000 monthly active users, 100 GB of files, 250 GB of egress, daily backups kept 7 days.
  • Team: from $599/month.

Pro overage rates: disk $0.125/GB, users $0.00325 each past 100,000, files $0.0213/GB, egress $0.09/GB ($0.03 cached). Point-in-time restore is an add-on at $100/month for 7 days, and a custom domain is $10/month. Bigger compute costs extra: Small (2 GB) about $15, Medium (4 GB) about $60.

Plain Postgres pricing in 2026

Here is what a database alone costs, roughly matched to Supabase Pro's 1 GB Micro instance.

Provider Smallest sensible plan Monthly price Backups
Supabase Pro Micro, 1 GB RAM, 8 GB disk $25 (includes auth, files, realtime) Daily, 7 days
Neon Launch Usage-based, billed per compute-hour Neon's own example: about $15 for 140 compute-hours and 1 GB Instant restore, $0.20/GB-month
Render 1 GB RAM $19 + storage at $0.30/GB Point-in-time restore, 3 days (Hobby) or 7 days (Pro)
DigitalOcean 1 vCPU, 1 GiB $15.15 with 10 GiB Daily backups + 7-day point-in-time restore
Hubfly space Small, 0.5 vCPU, 1 GiB, 10 GiB $3.92 No automatic backups yet (coming soon)

Two things stand out. Supabase Pro is a fair price once you count everything in it. And if you only need the database, you can pay a fraction of that.

Neon deserves a special mention: every plan now includes managed Better Auth, a Data API and Realtime at no separate charge, with auth for up to 60,000 monthly active users per project on Free and 1 million on paid plans. If what you want is "Postgres plus login", it's a strong middle ground.

Supabase vs Postgres: same app, two bills

Take a typical small SaaS: a 2 GB database, 10,000 monthly active users, 20 GB of uploaded files and live updates in the UI.

On Supabase Pro: $25.00 a month. Everything fits inside Pro's included amounts: 2 GB of the 8 GB disk, 10,000 of 100,000 users, 20 GB of 100 GB of files. The free plan won't do, because the database alone is over its 500 MB limit.

On plain Postgres plus your own pieces (Hubfly space): $9.09 a month.

Piece Size Monthly
Hubfly space managed PostgreSQL Small: 0.5 vCPU, 1 GiB, 10 GiB $3.92
Hubfly space Cell container (runs your auth library and API) 0.5 vCPU, 1 GB, 10 GB disk, always on $3.03
Hubfly space Storage 20 GB of files $0.22
Managed Realtime Nano $1.92
Auth Open-source library inside your API $0
Total $9.09

How each number is worked out (730 hours a month; CPU $0.004 per vCPU-hour, memory $0.002 per GB-hour, storage $0.00001536 per GB-hour):

  • Database: (0.5 × $0.004 + 1 × $0.002) = $0.004/h, plus the 30% management fee ($0.0012/h), plus 11 GiB of disk (10 GiB data + 1 GiB system) × $0.00001536 = $0.000169/h. Total $0.005369/h × 730 = $3.92.
  • API container: (0.5 × $0.004 + 1 × $0.002) × 730 = $2.92, plus 10 GB × $0.00001536 × 730 = $0.11. Total $3.03.
  • Files: 20 GB × $0.00001536 × 730 = $0.22.
  • Realtime: the Nano size is $1.92, with no per-message fee.

Network transfer isn't in either total. Supabase Pro includes 250 GB of egress; check your provider's bandwidth terms before comparing heavy-download apps.

When the math flips

The gap closes fast if you'd buy auth as a hosted service. Say the app grows to 150,000 monthly active users:

  • Supabase Pro: $25 + 50,000 extra users × $0.00325 = $187.50.
  • Clerk Pro (billed monthly) for sign-in alone, assuming all 150,000 count as Clerk's monthly retained users: $25 + 50,000 users × $0.02 + 50,000 users × $0.018 = $1,925.
  • An open-source auth library in your own API: $0 extra, but you own password resets, OAuth setup and security updates.

So Supabase's included users are a big deal if your alternative is a hosted auth service. They matter much less if you're happy running a library yourself.

When Supabase is the better choice

Be honest about what you'd rebuild. Pick Supabase when:

  • You'd otherwise buy hosted auth. 100,000 monthly active users are included in Pro.
  • Your app is mostly frontend. The auto-generated API plus row-level security means many apps need no backend server at all.
  • You want backups handled. Pro keeps 7 days of daily backups without you doing anything, and point-in-time restore is available as a $100/month add-on.
  • You're prototyping. The free plan gets you a database, login and file uploads in minutes. Just remember free projects pause after a week without activity.
  • You value one dashboard. One place for users, tables, files, logs and keys saves real time on a small team.

When plain Postgres is the better choice

  • You already have a backend. If your API handles auth and business logic, Supabase's auto-generated API goes unused.
  • You only need a database. A 1 GB managed Postgres costs $3.92 to about $20 elsewhere versus $25.
  • You want to own your auth data. Users live in your own tables, with no provider-specific schema.

The honest downside on Hubfly space today: managed Postgres runs on a single node, with no replicas, no high availability and no point-in-time restore, and automatic backups are still coming. Until they arrive, run your own pg_dump on a schedule from any machine or job runner you already have, and keep the files somewhere else (the dump and restore commands are in how to upgrade Postgres to a new major version in Docker). If you need automatic backups or failover today, Supabase Pro, DigitalOcean or Render are the safer picks.

Moving from Supabase to plain Postgres: the part guides skip

Most migration guides say "run pg_dump, then pg_restore". On a typical Supabase schema, that fails, because your tables and policies point at things that only exist inside Supabase: the auth schema, the auth.uid() function and roles such as authenticated.

Here's a typical table from a Supabase app:

create table public.todos ( id bigint generated by default as identity primary key, user_id uuid not null default auth.uid() references auth.users(id) on delete cascade, title text not null, done boolean not null default false ); alter table public.todos enable row level security; create policy "own todos read" on public.todos for select to authenticated using ((select auth.uid()) = user_id); create policy "own todos write" on public.todos for insert to authenticated with check ((select auth.uid()) = user_id); 

Dump only your own schema and restore it into plain PostgreSQL, and you get this:

pg_restore: error: could not execute query: ERROR: schema "public" already exists pg_restore: error: could not execute query: ERROR: schema "auth" does not exist LINE 3: user_id uuid DEFAULT auth.uid() NOT NULL, pg_restore: error: could not execute query: ERROR: relation "public.todos" does not exist ... pg_restore: warning: errors ignored on restore: 12 

The dangerous part: pg_restore keeps going. It finishes with a warning and exit status 1, and your todos table simply isn't there. Adding --no-owner --no-acl doesn't help; the table still needs auth.uid().

The fix is to recreate the small part of Supabase your schema depends on, before you restore.

Step 1: Dump your own tables

Supabase runs Postgres 17, and pg_dump must be the same major version as the server or newer, so use version 17 or newer. The flags below are the same in every recent version. Copy a direct or session-mode connection string from your Supabase project's Connect dialog.

pg_dump "$SUPABASE_DB_URL" --format=custom --schema=public --file=public.dump 

--schema=public leaves out Supabase's own schemas (auth, storage, realtime and others). Your users are in auth.users, so export the columns you need separately:

psql "$SUPABASE_DB_URL" -c "\copy (select id, email from auth.users) to 'users.csv' with csv header" 

Step 2: Add the shim to the new database

This recreates the three roles, a minimal auth.users table and auth.uid(). It reads the user ID the same way Supabase's own helpers do: first request.jwt.claim.sub, then the sub field of request.jwt.claims (Supabase's example). Save it as shim.sql:

-- Make plain Postgres understand Supabase-style policies do $$ begin if not exists (select from pg_roles where rolname='anon') then create role anon nologin; end if; if not exists (select from pg_roles where rolname='authenticated') then create role authenticated nologin; end if; if not exists (select from pg_roles where rolname='service_role') then create role service_role nologin; end if; end $$; create schema if not exists auth; create table if not exists auth.users (id uuid primary key, email text); create or replace function auth.uid() returns uuid language sql stable as $$ select coalesce( nullif(current_setting('request.jwt.claim.sub', true), ''), (nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub') )::uuid $$; grant usage on schema auth to anon, authenticated, service_role; grant execute on function auth.uid() to anon, authenticated, service_role; 

It works with a regular admin user that can create roles; no superuser needed. That matters on managed databases, where you never get superuser. (It's also why service_role here has no BYPASSRLS: a role that doesn't have it can't give it, and admin users on managed databases normally don't have it.)

The nullif(…, '') is not decoration. Once a setting has been used on a connection, Postgres returns an empty string for it afterwards, not NULL. Without nullif, the next request on that pooled connection fails with invalid input syntax for type json.

Step 3: Load users, then restore

psql "$NEW_DB_URL" -v ON_ERROR_STOP=1 -f shim.sql psql "$NEW_DB_URL" -c "\copy auth.users (id, email) from 'users.csv' with csv header" pg_restore --list public.dump | grep -v -E 'SCHEMA - public|COMMENT - SCHEMA public|DEFAULT ACL' > restore.list pg_restore --no-owner --exit-on-error --use-list=restore.list --dbname="$NEW_DB_URL" public.dump 

The list file skips two kinds of entries. CREATE SCHEMA public already exists and would otherwise be the first error. DEFAULT ACL lines are Supabase's default grants for its own roles (for example ALTER DEFAULT PRIVILEGES FOR ROLE postgres …); a non-superuser can't set them for another role, and the restore stops with permission denied to change default privileges.

Before restoring, list the grants the restore will run. Every role named there must exist in the new database:

pg_restore --file=- --use-list=restore.list public.dump | grep -E '^(GRANT|REVOKE)' 

If the restore still stops with role "…" does not exist, create that role with create role <name> nologin; and run the restore again on a fresh database. Don't delete the table's ACL line instead: it holds all of that table's grants, including the one to authenticated your policies need. For any other Supabase-only object the error names, remove its line from restore.list. --exit-on-error makes the restore stop at the first real problem instead of skipping tables. Keep the grants (no --no-acl): your policies need the authenticated role to have its table permissions.

If a column default calls extensions.uuid_generate_v4(), create that extension in a schema with the same name before restoring: create schema extensions; create extension "uuid-ossp" schema extensions;.

Step 4: Run queries as the signed-in user

Your policies now work, but only if each request tells Postgres who the user is. Create a login role for your app that does not own the tables, and let it act as authenticated:

create role shop_api login password 'a-long-random-password'; grant authenticated to shop_api; 

Then wrap each request in a transaction that switches role and sets the user ID. Here it is for Node.js 22 with pg 8:

const { Pool } = require('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); // Run queries as one signed-in user, so Supabase-style RLS policies apply. async function asUser(userId, fn) { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query('SET LOCAL ROLE authenticated'); await client.query("SELECT set_config('request.jwt.claims', $1, true)", [JSON.stringify({ sub: userId, role: 'authenticated' })]); const result = await fn(client); await client.query('COMMIT'); return result; } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } // Usage: const { rows } = await asUser(session.userId, c => c.query('SELECT * FROM todos')); 

SET LOCAL and the true in set_config scope both to the transaction, so the next request on the same pooled connection starts clean. With this in place, user A sees only A's rows, and when user B tries to insert a row for user A, Postgres refuses with new row violates row-level security policy for table "todos".

What can go wrong

  1. Every user sees every row. Your app connects as the table owner. Owners skip row-level security by default, so a policy that works on Supabase silently does nothing. Connect as a separate role like shop_api, or run alter table todos force row level security;.
  2. Every query returns zero rows. The role switch worked but the user ID didn't arrive, so auth.uid() is NULL. Check that set_config runs inside the same transaction as the query.
  3. pg_restore "finished" but a table is missing. You restored without --exit-on-error. Look for errors ignored on restore in the output, drop the new database (not your Supabase one) and restore again with the flag.
  4. server version mismatch from pg_dump. Your local pg_dump is older than the server. Install the PostgreSQL 17 or 18 client tools.
  5. Uploaded files are gone. Supabase backups don't include Storage objects, and neither does pg_dump: the database only holds their metadata. Copy the files with any S3 tool before you switch off the old project.
  6. Everyone has to log in again. Sessions and password hashes belong to Supabase Auth. Plan the move of logins separately; exporting id and email keeps your foreign keys intact, not your users' passwords.

FAQ

Is Supabase just Postgres?

The database is regular Postgres, and you can connect to it with any Postgres client or ORM. Supabase adds auth, storage, realtime, an auto-generated API and backups around it, plus a few schemas and roles of its own.

Do I need Supabase or just Postgres?

If you have your own backend and only need a database, plain Postgres is cheaper. If you'd otherwise buy hosted auth, or your app talks to the database straight from the browser, Supabase saves you real work.

How much does Supabase cost per month?

Free is $0 with a 500 MB database and pausing after a week of inactivity. Pro starts at $25 a month with 8 GB of disk, 100,000 monthly active users and 250 GB of egress. Bigger compute, extra users and point-in-time restore cost extra.

Can I move my Supabase database to another Postgres host?

Yes. Dump your own schema with pg_dump, recreate auth.uid() and the Supabase roles on the new database, then restore with --exit-on-error. Files in Supabase Storage and user passwords need their own migration.

Will my RLS policies work outside Supabase?

Yes, once auth.uid() exists and each request sets the user ID in a transaction, as in Step 4. Policies are plain Postgres; only the helper function is Supabase-specific.

Do Supabase backups include my uploaded files?

No. Supabase's backups cover the database only, which stores metadata about your files, not the files themselves.

What's the cheapest way to get Postgres plus login?

Run an open-source auth library in your own API next to a small managed database, or use Neon, whose plans include managed Better Auth at no separate charge. For a broader price list, see the cheapest database for a small app in 2026.

Go further on Hubfly space

Keep your Postgres, drop the bill you don't use. Create a managed PostgreSQL 18 database and pay for the size you pick. Try it on Hubfly space

By Octave · drafted with AI, reviewed by Octave · Last updated 6 November 2026