Skip to content
Engineering

D1 vs Postgres vs Supabase, Choosing App Storage

BYOB Team

BYOB Team

8 min read

Choose D1 for simple edge apps that need serverless SQLite storage with no database to operate. Choose Supabase for managed Postgres plus auth, storage, realtime, and row level security. Choose direct Postgres only when you must own the database. Serverless functions need pooled connections with prepared statements off, and Drizzle works across all three paths.

Key takeaways

  • • D1 fits simple edge apps with serverless SQLite and no database to operate
  • • Supabase adds managed Postgres plus auth, storage, realtime, and row level policies
  • • Direct Postgres maximizes control but hands you backups, pooling, and upgrades
  • • Serverless functions must pool with size one and prepared statements off, and Drizzle spans all three
D1 vs Postgres vs Supabase, Choosing App Storage

Short answer, ownership versus platform versus simplicity #

Choose Cloudflare D1 when you want the simplest durable storage for an edge app: SQL semantics you already know, no server to operate, a natural fit for users, sessions, and app records.

Choose Supabase when you want managed Postgres plus an integrated platform: database, auth helpers, file storage, realtime subscriptions, and policy tooling in one place.

Choose direct Postgres when you must own the database itself: custom extensions, existing infrastructure, migration control, or compliance rules that forbid a platform.

New apps on the edge should default to D1. Move only when product requirements pull you off it, with evidence in hand.


flowchart TD A[New app needs storage] --> B{Need auth, storage, realtime & RLS out of box?} B -->|No, simple records| C[Use D1: serverless SQLite on the edge] B -->|Yes, want platform| D[Use Supabase: managed Postgres + platform services] D --> E[Pool with PgBouncer transaction mode, pool 1, prepared off] C --> F[No ops, D1 binding] B -->|Must own DB or custom extensions| G[Run direct Postgres] G --> H[You handle backups, pooling & upgrades]

What is each option? #

D1 is Cloudflare's managed serverless database with SQLite semantics, built for queries from Workers and Pages projects, with built-in disaster recovery and access over Worker bindings or HTTP API, as stated in the Cloudflare docs (D1 docs). You create thousands of databases at no extra cost, which suits platforms that isolate tenants per database, and pricing follows queries and storage rather than reserved capacity.

Postgres is the full relational database: rich SQL, constraints, transactions, extensions, and decades of operational knowledge. Run it yourself or through a provider. The power arrives with responsibility for backups, upgrades, monitoring, and connection management, unless someone else carries that pager.

Supabase is a managed platform built on Postgres. Beyond the database it provides auth, file storage, realtime, and edge functions. It is Postgres plus an opinionated backend around it, with client libraries that wrap Data APIs and handle authentication, as stated in the Supabase docs (connecting to Postgres).

Drizzle ORM spans all three paths. It is a headless TypeScript ORM with relational and SQL-like query APIs, dialect support across Postgres, MySQL, and SQLite, and zero dependencies by design, as stated in the Drizzle docs (overview). If you know SQL, the learning curve stays near flat. The adapter and connection pattern still differ per runtime, and that difference is where serverless apps usually break.


Comparison table #

Criterion Cloudflare D1 Supabase Direct Postgres
Mental model SQLite at the edge Managed Postgres plus services Postgres you operate
Operational burden Low Low to medium High
Auth, storage, realtime included No, compose separately Yes, platform primitives No, build or integrate
Row-level security Application checks First-class database RLS Available, you configure it
Backups and recovery Built in, 30-day Time Travel Managed backups Your runbooks
Serverless connections Binding model, no pooling pain Transaction pooler required Pooling plus ops discipline
Portability and exit Export code Postgres foundation eases export Highest control, you own everything

No row wins everywhere. Simplicity trades against platform breadth, which trades against direct control.


Which decision rules actually hold? #

Stay on D1 while the schema stays simple: users, sessions, saved records, straightforward relations. D1 sweetens the deal with 30-day point-in-time recovery through Time Travel and global read replicas that lower read latency by copying data closer to readers, as stated in the Cloudflare docs (D1 docs). If realtime channels, cross-service policies, or Postgres-only features have not appeared, the default holds.

Move to Supabase when you can name the platform feature you need. Managed Postgres plus auth helpers. File storage with access policies. Realtime subscriptions on database changes. Row-level security enforced where no application bug can bypass it. Vague scalability feelings do not count. Named features count.

Run direct Postgres when ownership is the requirement. Existing Postgres fleets, specific extensions, network topology mandates, backup regimes auditors must see. Budget honestly: pooling, migrations, monitoring, upgrades, on-call rotations. Control is never free.

A useful graduation signal is access-control complexity. Simple per-user ownership fits D1 plus application checks. Declarative per-row rules across many roles points toward database-enforced policies.


Try it: Tech Stack Picker

Try it right here: tech stack pickerOpen full tool

Loading the interactive tool… or open it here.

Where does row level security earn its keep? #

Postgres restricts rows per user through policies that ride alongside the standard grant system. When enabled on a table, every row read or write must pass a policy, and tables without policies default to denying everything, as stated in the Postgres docs (row security policies). Policies can target commands, roles, or both, and combine with OR for permissive rules or AND for restrictive ones.

sql
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;

CREATE POLICY account_managers ON accounts TO managers
  USING (manager = current_user);

Supabase turns this primitive into a workflow. Enable RLS on every table in an exposed schema, revoke the automatic grants, grant back only what each role needs, write one policy per operation, and prove it with database tests, as stated in the Supabase docs (row level security). A table in an exposed schema without RLS stays readable and writable by any role holding a grant, which the docs flag as an explicit danger, as stated in the Supabase docs (row level security).

sql
alter table profiles enable row level security;
revoke all on table profiles from anon, authenticated;
grant select, insert, update, delete on table profiles to authenticated;

create policy "Users can view their own profile."
on profiles for select
to authenticated
using ( (select auth.uid()) = user_id );

Because policies run inside the database, they protect data even when reached through third-party tooling. That defense-in-depth quality is the real argument. Application checks work until the one code path that forgot them.

D1 has no equivalent primitive. Application code owns authorization, which is fine for simple ownership and risky for complex sharing rules. When policies multiply across roles, that is the migration signal.


Connection pooling, where serverless apps die #

Serverless runtimes break the one-server-one-pool assumption. Functions scale out fast, each instance opens connections, and traditional Postgres runs out under burst traffic. Supabase answers with explicit connection guidance: serverless and edge functions use the shared pooler in transaction mode, as stated in the Supabase docs (connecting to Postgres).

The client recipe is precise. Create the client once at module scope. Set the pool to one connection. Turn off prepared statements, which transaction mode does not support. Require SSL. The docs show the shape:

ts
import postgres from 'postgres'

export const sql = postgres(process.env.DATABASE_URL, {
  max: 1,
  prepare: false,
  ssl: 'require',
})

Library defaults assume persistent backends and will exhaust the pool from a few dozen warm instances. Raise the pool above one only with evidence of queuing, not optimism.

With Drizzle, match the driver and adapter to the runtime. An adapter that works locally can fail at the edge, and transaction-mode limits on prepared statements, cursors, and session state apply regardless of ORM. The D1 path sidesteps most of this through platform bindings rather than classic connections. Switching to Supabase or direct Postgres makes pooling your responsibility again.


What we learned building this #

Our platform provisions a per-project edge database with schema access and optional managed Postgres with auth plus storage plus realtime. The choice in the post mirrors real BYOB usage: simple ownership stays on the edge database and per row security or realtime points to managed Postgres.

Who this is for (and who should skip it) #

This fits edge apps and SvelteKit builders who want minimal ops for users sessions and app records. If you prefer SQLite semantics with built in Time Travel and global reads D1 is a calm default.

Pick managed Postgres or Supabase when you can name the platform feature you need such as row level security realtime or storage policies enforced in the database. Move when product requirements say so not on vague scale worry.

One limit to know. D1 fits simple app records, but realtime or row level rules point to Postgres sooner. A common mistake is choosing on hypothetical scale instead of a named feature the product needs today.

  • Best for developers wanting low ops storage for sessions and app records.
  • Best for startups naming a need for row level security or realtime.
  • Best for small teams starting on D1 and moving on clear product evidence.

Verdict, default to D1 and graduate on evidence #

New apps: D1. Common case, least operational surface, accounts and saved data handled.

Name the Supabase feature before moving: auth helpers, storage policies, realtime, RLS. Not vibes. Features.

Choose direct Postgres when ownership itself is the requirement, and price the pager honestly.

Revisit when schema, access rules, or realtime needs change. Not before. Databases are easy to adopt and expensive to migrate, so the cheapest correct choice today usually wins tomorrow too.

How we picked these

We picked the three stores teams actually face on our platform. We compared them on ownership ops auth and realtime using Cloudflare D1 docs Supabase guides Postgres RLS docs and our own database clients.

Frequently asked questions

When is D1 the right default?

When the schema is users, sessions, and straightforward app records on the edge. D1 offers serverless SQL with SQLite semantics, thousands of databases at no extra cost, 30 day point in time recovery, and read replicas for global reads

When should Supabase be chosen instead?

When you need managed Postgres plus platform services: auth helpers, file storage, realtime, and row level security enforced in the database. Accept a second platform with its own client and policy patterns

Why does connection pooling matter on serverless?

Functions scale out fast with many short lived connections. Supabase directs serverless code to transaction mode poolers, with client pools set to one connection and prepared statements disabled

How does row level security differ across the options?

Postgres and Supabase enforce RLS inside the database with default deny policies per operation. D1 relies on application checks instead, which suits simple ownership but not complex multi role rules

Changelog

  • • Added fit guide, comparison table, and hands on notes

About the Author

BYOB Team

BYOB Team

The creative minds behind BYOB. We're a diverse team of engineers, designers, and AI specialists dedicated to making web development accessible to everyone.

Ready to start building?

Join thousands of developers using BYOB to ship faster with AI-powered development.

Get Started Free