Skip to main content
ASoc
Comparison

Supabase vs Turso: Four Platform Pieces, and Who Replaces Them

Postgres with RLS, auth and storage versus edge SQLite. An inventory of what a move off this storefront's Supabase stack would actually have to rebuild.

The ASoc Team8 min read

Supabase is a Postgres platform: the database arrives with an auth service, object storage, a REST gateway, and row-level security as the enforcement point. Turso is a database — hosted libSQL, a fork of SQLite, built for edge replication and for creating databases cheaply enough that one per tenant is reasonable. Choosing between them is mostly choosing how much of your backend you want to not write.

This storefront runs on Supabase. What follows is an inventory, taken from this repository, of exactly which of our code depends on the platform rather than on a SQL database — because that is the part a move to Turso would have to replace. We have not rebuilt this stack on Turso, so nothing here is a benchmark of it; it is a measurement of the switching surface.

Where the two actually diverge

SupabaseTurso
EnginePostgreslibSQL (SQLite fork)
Authorization in the databaseRow-level security policiesNone — SQLite has no RLS
Auth serviceIncluded (sessions, OAuth, JWTs)Bring your own
Object storageIncluded, with private bucketsBring your own
Auto-generated APIPostgREST over your schemaClient SDK over SQL
WritesConcurrent, MVCCSerialized to a single primary
Replication modelRead replicas, regionalEdge replicas and local embedded replicas
Database-per-tenantUnusual — one database, many rowsNormal — databases are cheap to create
Server-side functionsplpgsql, SECURITY DEFINER, advisory locksSQL, without Postgres's procedural surface

Read that table as one question: does your authorization live in the database? If it does, the move is a rewrite of your security model. If it does not, the two are much closer than their marketing suggests.

The inventory: what a move would have to replace

Four things in this repository are Supabase-the-platform rather than Supabase-the-Postgres.

Six row-level security policies. All for select, all read-own, created by two migration files:

create policy "own orders" on public.orders
  for select using ((select auth.uid()) = user_id);

There is no INSERT policy anywhere — every write goes through server code holding the service-role key, which bypasses RLS entirely. On Turso that policy does not have a home. The check moves into application code, where it becomes a where user_id = ? you must remember on every query, and the guarantee changes from "the database will not return another user's row" to "we did not write a query that returns another user's row". Those are different promises. What the service role key actually does covers why the write path already lives outside RLS here.

An auth service. src/proxy.ts runs on every request that is not a static asset and refreshes the session by validating the JWT:

await supabase.auth.getClaims();

Email/password, Google OAuth, password reset and the callback route are all Supabase Auth. Turso has no equivalent, so a move means adopting a separate auth provider or building sessions yourself — and then re-deriving the user id that every where clause above now depends on.

A private storage bucket. Every paid download is a signed URL over a private releases bucket. Turso stores rows, not zip files, so this becomes S3, R2 or equivalent, plus the signing code that goes with it.

Two SECURITY DEFINER RPCs, and this is the interesting one, because it is the only place where SQLite's model is arguably an advantage. 0007_atomic_download_rate_limit.sql exists because the download route originally counted recent downloads and then inserted an audit row as two separate calls, so concurrent requests could all pass the same check. The Postgres fix was one RPC holding a transaction-scoped advisory lock. SQLite serializes write transactions to a single primary by design — the race the advisory lock closes is closed for you, and the cost is that you cannot have concurrent writers at all.

That trade is the whole comparison in miniature: Postgres gives you concurrency and asks you to reason about it, SQLite removes the reasoning by removing the concurrency.

What would port unchanged

Less would break than the list above suggests, because the rules are not written in SQL:

export function authorizeDownload(
  slots: Slot[],
  target: DownloadTarget,
): boolean {
  return slots.some((s) => slotCovers(s, target));
}

src/lib/entitlements.ts decides what a buyer may download, takes plain objects, imports nothing, and would run identically on either database. The payment webhook is the same shape — processWebhook talks to a two-method WebhookDb interface, and the Supabase implementation of it is 43 lines in a file of its own. Swapping the database means rewriting that file and leaving 287 lines of decision logic alone. The suite proves it: 31 test files, 357 tests, 3.3 seconds, no database of any kind.

Two of this app's twelve runtime dependencies are Supabase packages. That is the honest measure of coupling at the dependency level — and it understates it, because the four platform features above are not packages.

Where Turso is the straightforwardly better answer

  • Database per tenant. If every customer gets their own database — a genuinely good isolation story — Turso is designed for it and Postgres is not.
  • Read latency at the edge. Embedded replicas put a local SQLite file next to your compute and sync it from the primary. Nothing in Postgres-land matches that for read-heavy, globally distributed workloads.
  • Read-mostly workloads generally. Single-writer is a hard ceiling on writes and no constraint at all on reads.
  • You already have auth and storage. If Clerk or Auth.js is handling sessions and S3 is holding files, most of Supabase's bundled value is already bought elsewhere, and what remains is "a Postgres".

And where Supabase is: you want authorization enforced by the database, you want auth and file storage without choosing two more vendors, and your write path has genuine concurrency — which for this storefront it does, since a payment webhook, a redemption and a download audit can all land at once.

The question that decides it

Not "Postgres or SQLite". It is: how many of the four platform pieces above would you have to go and buy?

Count them honestly. If the answer is zero because you already run auth and storage elsewhere, Turso is competing on the database alone, and it competes well. If the answer is three, you are not comparing databases — you are comparing one vendor against a stack you have not assembled yet, and the comparison should say so. Supabase vs. Neon asks the same question where both sides are Postgres, which isolates the platform half of the decision from the engine half.

Mistakes and how they show up

SymptomCauseFix
Users see each other's rows after a migration off PostgresRLS was the only enforcement; the new database has noneMove the ownership check into a shared query layer, not into each call site
Write throughput plateaus under load on TursoSQLite serializes writes to the primaryBatch writes, or keep write-heavy tables on a database built for concurrency
A read right after a write returns stale dataReplicas sync asynchronouslyRead through the primary for read-your-writes paths
Case-insensitive email lookups start missing rowscitext is a Postgres extensionUse a NOCASE collation, and normalise on write
Migration history stops being reproducibleSchema changes made in a dashboard rather than in filesKeep every change as a numbered migration on either platform
"It is just SQL, the port is mechanical"Extensions, procedural functions and policies are not portable SQLInventory those three before estimating the move

Frequently asked questions

Is Turso a drop-in replacement for Supabase? No. It replaces the database. Auth, object storage, the auto-generated API and row-level security are Supabase features with no Turso equivalent, so a migration is a backend re-architecture unless you were already using Supabase purely as a Postgres host.

Does SQLite scale for a production app? For reads, very well — that is the premise of embedded replicas. For writes, it serializes to one primary, which is fine for most applications and disqualifying for a few. Know which you are before you choose.

Can I enforce per-user access on Turso? In application code, yes. In the database, no — there is no row-level security. Whether that matters depends on whether every query already goes through one data-access layer you can audit, or through fifty call sites you cannot.

What is the smallest change that keeps this decision reversible? Put the authorization rules in pure functions and the data access behind a narrow interface. In this repo that is authorizeDownload(slots, target) plus a two-method WebhookDb — which is why the database-specific code here is a 43-line file rather than a layer.

Templates in this post

ASoc Vault is a fintech SaaS landing page. ASoc Vox is an AI voiceover product site. ASoc Weave is an AI website-builder landing page. Each is the front end for a product whose backend decision looks exactly like the one above.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison10 min read

Supabase vs Vercel: Both, and 42 Lines Where They Meet

Not alternatives. Vercel builds and serves 584 pages, Supabase holds the schema and the policies, and one 42-line proxy can take the whole site down.

Read more
Comparison9 min read

SvelteKit vs. Remix: Same Primitives, Different Component Bill

Both answer routing, data loading and form posts the same way. The divergence is what reaches the browser — and how much of a page you can avoid shipping at all.

Read more
Comparison10 min read

Tailwind CSS v4 vs Bootstrap 5 for Dashboard UIs in 2026

A fair comparison of two mature CSS frameworks for admin UIs — component coverage, customization ceiling, bundle size, and the team each one suits.

Read more