Cloudflare D1 vs Supabase — Decision Matrix for Solo SaaS

7-axis decision matrix: Cloudflare D1 vs Supabase for solo SaaS. Cost, latency, DX benchmarked on 3 production apps. TanStack Ship ships both pre-wired.

Huifer
Huifer
May 23, 20266 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I have shipped three solo SaaS apps in the last 14 months — one Cloudflare D1 native, one Supabase native, one hybrid — so this Cloudflare D1 vs Supabase matrix is from real telemetry, not vendor docs. Two serve under 10k MAU; one serves a B2B workload at ~3k MAU with multi-tenant RLS. Every claim links to a first-party source, every limit is bounded by my own deployment.

Verified sources: Cloudflare D1 documentation · D1 limits and pricing · D1 read replicas · Supabase documentation · Supabase pricing · Supabase RLS · Drizzle ORM · better-auth · Last updated: 2026-05-23 · Changelog


TL;DR: For a Workers-native solo SaaS in 2026, D1 wins on cost and latency at sub-10k MAU. Supabase wins when you need Postgres features (JSONB, LISTEN/NOTIFY), built-in auth, and storage in one bill. The honest answer for ~60% of solo founders is both: D1 for the core workload, Supabase for auth + storage + realtime. This article walks through the 7-axis matrix and the production telemetry behind every score.


Cloudflare D1 vs Supabase — Decision Matrix for Solo SaaS

Most "D1 vs Supabase" articles read like vendor blog posts — they score the alternative on its weakest axis and ship the vendor answer. This is the opposite. I have shipped both in production, and the matrix below scores each on seven axes a solo founder actually cares about: cost, latency, auth, realtime, storage, schema, and operational overhead.

The Data-Layer Choice That Frames the Whole Stack

The database decision in 2026 is not "Postgres vs SQLite." It is "which Postgres-shaped bundle, which SQLite-shaped engine, and which auth/storage/realtime come for free with the choice." Supabase ships Postgres + GoTrue auth + S3 storage + Realtime WebSockets + Edge Functions in one bill. D1 ships SQLite + read replicas + Better Auth integration.

Three Forces Reshaping Solo SaaS Data in 2026

Edge runtimes — Cloudflare Workers, Vercel Edge, Deno Deploy — have moved compute close to users. The "Postgres on a $40/mo VM in us-east-1" pattern is a competitive disadvantage for any product serving global traffic.

Serverless Postgres — Neon, Supabase, PlanetScale — replaced the always-on VM with per-usage pricing. A solo founder can run a real Postgres for $25/mo.

Distributed SQLite — D1, libSQL/Turso, and Durable Objects have turned SQLite into a globally replicated database with sub-millisecond regional reads.

The 7-Axis Decision Matrix

Each axis below is scored 1–5 for both engines, anchored in my production telemetry at sub-10k MAU.

AxisD1SupabaseEdgeNotes
1. Cost at <10k MAU53D1Free tier is real; Supabase pauses on free
2. Latency from Workers/Edge53D1D1 reads co-located with the Worker
3. Auth and Row-Level Security35SupabaseGoTrue + RLS is mature
4. Realtime Subscriptions25SupabasePostgres replication slots + WS
5. Storage Bundle25SupabaseS3-compatible, signed URLs, transforms
6. Schema Evolution44TieDrizzle migrations work both ways
7. Operational Overhead53D1One Cloudflare bill vs three dashboards

Total: 26 vs 28. On paper, Supabase wins by two. In practice, the cost axis dominates at <5k MAU and the latency axis dominates for global SaaS.

Axis 1 — Cost at <10k MAU

D1's free tier is 5 GB stored + 5M rows read + 100k rows written per day. My D1-native app shipped on the free tier for 11 months before crossing 100k writes/day.

Supabase's free tier pauses after 7 days of inactivity. The Pro plan starts at $25/mo. My Supabase-native app crossed into Pro at month 2 and hit $45/mo by month 9 (~$3k MAU, mostly storage egress).

D1 wins for any SaaS not yet charging customers.

Axis 2 — Latency From Workers / Edge

D1 reads execute in the same region as the calling Worker. My us-east and eu-west replicas serve p50 read latency under 5ms.

Supabase sits in a single region (or two on the multi-region add-on). From a Worker in eu-west, my Supabase-native app measured p50 round-trip at 110ms; from us-east, 35ms. The Supabase docs recommend Supavisor for serverless.

D1 wins for global SaaS. Supabase wins for one-region users who need Postgres features.

Axis 3 — Auth and Row-Level Security

Supabase's GoTrue ships with email/password, magic link, OAuth (30+ providers), MFA, and Postgres-native RLS. RLS is the killer feature — SQL policies enforce row-level access at the database layer, so application code cannot leak another tenant's rows.

D1 has no native auth. You pair it with Better Auth and enforce isolation in application middleware.

Supabase wins for any SaaS where multi-tenant isolation is a hard requirement. D1 + Better Auth is viable when you control every query.

Axis 4 — Realtime Subscriptions

Supabase Realtime is built on Postgres replication slots. You subscribe to row-level changes via WebSocket; the server pushes diffs to clients.

D1 has no native realtime. Workarounds are: poll with setInterval, use Cloudflare Durable Objects for WebSocket fanout, or push to a separate service.

Supabase wins decisively. If realtime is a core feature (chat, dashboards, presence), Supabase saves you a service.

Axis 5 — Storage Bundle

Supabase Storage is S3-compatible with signed URLs, image transforms, and per-bucket RLS. My Supabase-native app stores ~120 GB of user uploads at $0.021/GB/mo.

D1 has no native storage. You pair it with Cloudflare R2 (no egress fees).

R2 + D1 wins on cost at >50 GB. Supabase wins on integration — one bill, signed URLs that respect RLS.

Axis 6 — Schema Evolution

Drizzle migrations work identically against both engines. Both handle additive changes cleanly; neither handles destructive changes without a backfill plan.

Tie. Identical friction.

Axis 7 — Operational Overhead

D1 lives in the Cloudflare dashboard — backups, observability, billing — one place. My D1-native app's monthly ops checklist is 15 minutes.

Supabase has its own dashboard for database + auth + storage + realtime + edge functions, plus Cloudflare for the Worker runtime — three places to check when something breaks.

D1 wins for solo founders who want one bill and one dashboard. Supabase wins for teams that prefer a single vendor for the data layer.

Where Each One Wins (Honest Acknowledgment)

When D1 Is the Right Pick

Pick D1 if your SaaS is read-heavy, global, and under 10k MAU. Read-heavy dashboards, content sites, B2C tools, and most MVPs fit. Pair with R2 for storage and Better Auth for auth, and you have a single-vendor stack on one bill.

When Supabase Is the Right Pick

Pick Supabase if your SaaS needs Postgres features (JSONB, full-text search), multi-tenant RLS at the database layer, realtime subscriptions, or RLS-aware signed URLs. B2B SaaS with hard tenant isolation, live dashboards, and user-generated content all fit.

Running D1 and Supabase Side by Side in TanStack Start

The hybrid pattern — D1 for the core workload, Supabase for auth/storage/realtime — is what TanStack Ship ships by default.

Drizzle Schema That Spans Both Engines

ts
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { pgTable, uuid, timestamp, jsonb } from 'drizzle-orm/pg-core';

export const tenants = sqliteTable('tenants', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
});

export const auditLog = pgTable('audit_log', {
  id: uuid('id').defaultRandom().primaryKey(),
  tenantId: uuid('tenant_id').notNull(),
  payload: jsonb('payload').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
});

tenants lives in D1 (hot path). audit_log lives in Supabase (cold path, JSONB for varied payload shapes). One Drizzle config, two engines, one query API.

Query Helpers — Runtime Branching in 6 Lines

ts
// src/db/index.ts
import { drizzle as drizzleD1 } from 'drizzle-orm/d1';
import { drizzle as drizzlePg } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

export function getDb(env: Env) {
  const d1 = drizzleD1(env.DB, { schema });
  const pg = drizzlePg(postgres(env.SUPABASE_URL), { schema });
  return { d1, pg };
}

Application code calls db.d1.select().from(tenants) or db.pg.select().from(auditLog) based on which table it needs. No abstraction penalty.

Auth Adapter That Maps Better Auth → Supabase RLS

ts
// src/auth.ts
import { betterAuth } from 'better-auth';
import { Pool } from 'pg';

export const auth = betterAuth({
  database: env.DB,                    // D1 for user records
  secondaryStorage: new Pool({         // Supabase for session mirror
    connectionString: env.SUPABASE_URL,
  }),
  user: {
    additionalFields: {
      tenantId: { type: 'string', required: true },
    },
  },
});

The user record lives in D1 (fast auth lookups on every request); the session mirror lives in Supabase (so RLS policies can read auth.uid() from the same JWT).

Production Telemetry From 3 Solo SaaS Apps

AppEngineMAUMonthly costp50 read latency
B2C content SaaSD1 native4,200$5 (R2 egress)4ms
B2B analytics SaaSSupabase native2,800$45 (Pro + storage)35ms (us-east)
B2C marketplaceD1 + Supabase hybrid6,500$25 (D1 paid + Supabase free)5ms (D1) / 110ms (Supabase from eu-west)

The hybrid app is the cheapest at the highest MAU. The two engines talk via signed webhook events; the contract is in /db/schema.ts.

When to Pick Each (Decision Tree)

Is your SaaS read-heavy and global?
├── YES → D1 (or D1 + R2 + Better Auth)
└── NO → Do you need JSONB, LISTEN/NOTIFY, or full-text search?
        ├── YES → Supabase
        └── NO → Do you need multi-tenant RLS at the DB layer?
                ├── YES → Supabase
                └── NO → Do you need realtime subscriptions?
                        ├── YES → Supabase
                        └── NO → D1

If your SaaS spans multiple branches (read-heavy global traffic AND realtime chat AND multi-tenant B2B), pick D1 + Supabase hybrid — what TanStack Ship ships by default.

Frequently Asked Questions

Can I migrate from Supabase to Cloudflare D1 later?

Yes, with a Drizzle-based migration and a Better Auth replacement for GoTrue. Schema features you lose: JSONB queries (replace with TEXT + json_extract), LISTEN/NOTIFY (replace with Durable Objects or polling), database-level RLS (replace with application middleware).

Can I migrate from Cloudflare D1 to Supabase later?

Yes, with Supabase's pgLoader guide or a custom Drizzle export. You gain RLS, realtime, and storage.

Does Cloudflare D1 scale past 10k MAU?

I have not tested past 6,500 MAU on D1. D1 read replicas launched in late 2025; Cloudflare's published limits allow 10 GB per database and 100k writes/day on paid plans.

Does Supabase scale past 10k MAU?

Yes — Supabase pricing tiers go to enterprise. The catch at solo-founder scale is connection pooling: a fresh connection per Worker invocation exhausts Postgres max_connections long before the database is the bottleneck.

Is Supabase free tier enough for production?

For a SaaS that has not launched yet, yes. For one with paying customers, no — the free tier pauses after 7 days of inactivity.

Does TanStack Ship include both D1 and Supabase?

Yes. The default scaffold wires Drizzle against both engines. See features for the module list.

The Bottom Line

D1 and Supabase are complementary engines for two different SaaS shapes. D1 wins on cost and edge latency for read-heavy global SaaS. Supabase wins on auth, RLS, realtime, and storage for B2B SaaS with hard tenant isolation. The honest answer for most solo founders is both: D1 for the hot transactional path, Supabase for the auth/storage/realtime bundle.

TanStack Ship ships this hybrid by default. The features page has the full module list, pricing lists the tiers.


Next step: See TanStack Ship's data layer → · Read the D1 production guide → · Compare Ship to other SaaS boilerplates →