Skip to main content
ASoc
Comparison

Convex vs. Turso: Reactive Backend, Edge SQLite, or Neither

Edge read latency only helps if reads block a request. Here they do not -- so the 68 KiB auth bundle was the real latency, and a regex fixed it.

The ASoc Team8 min read

Convex is a complete reactive backend — document database, TypeScript server functions, file storage, scheduled jobs, live subscriptions. Turso is a database only: edge-distributed SQLite (libSQL), replicated close to your users for low read latency, and self-hostable. They are not competitors so much as answers to different questions.

Put them in a feature table and they look comparable. They aren't. One replaces your backend; the other replaces your database's location. Choosing between them means deciding which of two problems you actually have: too much backend plumbing, or reads that are too far away. And there is a third possibility worth checking first — that your read path never touches a database at all, which is the case on the site this post is published on.

The comparison that matters

ConvexTurso
What it isFull backend platformDatabase (SQLite / libSQL)
Data modelDocumentsRelational SQL
Query languageTypeScript functionsSQL
Real-timeReactive queries, automaticNot built in
Edge distributionNo — managed regionsYes, global replicas
Read latency storyOne region, cached client-sideReplica near the user
WritesTransactional mutationsSingle primary, replicas follow
Self-hostableNoYes — libSQL is open source
Server functionsIncludedNone — you bring a backend
AuthIntegrates an external providerNone — you bring one
Embedded / local replicaNoYes, a real differentiator
Exit costRewrite queries and move dataIt's SQLite; dump and go

Two rows carry most of the weight. "Server functions" means Convex is a destination and Turso is a component: pick Turso and you still need somewhere to run code, an auth provider, and a file store. "Writes" is Turso's asymmetry — replicas make reads fast and do nothing for writes, which still go to the primary. A write-heavy or read-after-write-sensitive app gets much less from edge distribution than the marketing implies.

The question to ask before either: where do your reads come from?

Edge read latency is only worth paying for if a user-facing request blocks on a database read. On this storefront, almost none do.

The public surface — the home page, 111 product pages, 7 category hubs, 311 blog posts, pricing, docs, the legal pages — is statically generated. The HTML is built once and served from a CDN. There is no query in the request path, so a replica 20ms away and a primary 200ms away produce byte-identical response times: zero database time either way. Turso's core benefit has nothing to attach to.

The database is reached from exactly four places, and a grep for Supabase imports across src/app finds them:

src/app/dashboard/              — the signed-in buyer's library
src/app/api/download/route.ts   — gated download, authorized per request
src/app/api/webhooks/lemonsqueezy/route.ts — purchase fulfilment
src/app/auth/callback/route.ts  — OAuth code exchange

Every one of those is per-user and authenticated. That matters more than it first appears, because per-user reads are the reads an edge replica helps least with: they cannot be shared between users, cannot be cached at the CDN, and in this codebase they are deliberately filtered by row-level security keyed on the caller's identity. An edge replica would make a dashboard query faster for one person at a time while the 99% of traffic that is anonymous and static was never querying anything.

That is the honest shape of a content-and-commerce site: a huge static read path and a small authenticated one. Neither edge SQLite nor a reactive backend is aimed at it.

The measurement that did matter

The latency that was actually costing this site was not database latency. It was the auth client's bundle cost, and it was on every page.

@supabase/ssr plus auth-js is ~68 KiB over the wire, 255 KiB parsed. Two components pulled it in at module scope: Header, which renders site-wide, and useOwnedProducts, which renders behind every templates grid. The result was the entire auth stack sitting in the initial bundle of /, /blog, /docs and /pricing — pages with no account UI whatsoever — downloaded and parsed before the page could settle.

Both call sites only ever touch the client inside an effect, so the import can wait for the effect too. src/lib/supabase/lazyClient.ts defers it, and then skips it entirely for signed-out visitors with a cookie probe:

export function hasAuthCookie(): boolean {
  if (typeof document === "undefined") return false;
  return /(?:^|;\s*)sb-.+?-auth-token/.test(document.cookie);
}

export async function loadSupabaseClient(): Promise<BrowserClient> {
  const { createClient } = await import("@/lib/supabase/client");
  return createClient();
}

@supabase/ssr stores the session in sb-<project-ref>-auth-token cookies that are deliberately not HttpOnly, because createBrowserClient reads them from document.cookie itself — so the probe sees exactly what the client would. No cookie means there is nothing for the auth stack to find, and 68 KiB never ships.

The security property that makes this acceptable is worth stating plainly, because "skip the auth check when a cookie is missing" sounds alarming: it is a loading shortcut for rendering only, and it can never grant anything. Every real gate stays on the server — authorizeDownload re-runs per request, and the RLS policies run in the database. A false negative costs at most a stale "Sign in" affordance until the next document load, which is also when signing in or out lands, since both round-trip the page.

That is a ~68 KiB win on first load for most visitors, from a three-line regex. Compare it to what moving the database to the edge would have returned on the same pages: nothing, because those pages don't read the database. Measure which hop your users are actually waiting on before you buy a solution to one of them.

When each one is right

Choose Turso when reads genuinely happen in the request path, are globally distributed, and are mostly reads: a documentation site with dynamic content, multi-tenant apps where each tenant can have its own database (Turso makes database-per-tenant cheap, which is a real architectural unlock), or anything that wants an embedded local replica with SQLite's semantics. Also choose it when self-hosting or a clean exit is a requirement — it is SQLite underneath, and that is the lowest lock-in of any option here.

Choose Convex when there is no backend and the app is genuinely reactive: collaborative editing, live dashboards, presence, chat. Reactive queries and transactional mutations remove plumbing you would otherwise hand-build, and the lack of operational surface is the point. Accept the vendor coupling — queries are TypeScript functions against their store, not portable SQL.

Choose neither when your read path is static. Prerender it, serve it from a CDN, and keep the database for the authenticated minority. That is what ships here: 311 posts and 111 product pages with no query between the request and the response, and a Postgres instance that only answers to signed-in users.

Mistakes and troubleshooting

SymptomCauseFix
Edge replicas didn't improve page speedReads weren't in the request path, or the page is staticProfile a real request first; move the work that's actually blocking
Read-after-write returns stale data on TursoReplicas follow the primary asynchronouslyRead from the primary after a write, or accept the lag deliberately
Writes are no faster after adding replicasWrites always go to the single primaryEdge distribution is a read-path optimisation only
Convex queries can't express a join you needDocument store, not relationalModel the access pattern, or pick a SQL database
Turso project still needs auth, functions, storageIt is a database, not a backendBudget for the other three services
Auth library inflates the bundle of pages with no account UIClient imported at module scope in a site-wide componentImport it inside the effect that uses it; probe the cookie first
Per-user data seems uncacheable at the edgeIt is — correctlyCache the static shell, fetch the per-user slice client-side

Frequently asked questions

Is Turso faster than Convex? For geographically distributed reads in the request path, usually yes — that is what edge replicas are for. For everything else the comparison doesn't hold: Convex's client-side reactive cache can make a repeat read effectively free, and Turso has no server functions to compare against.

Can I use Turso with Next.js on Vercel? Yes, over libSQL's HTTP client, which works in serverless and edge runtimes. Place replicas near your functions' regions, not just near your users, or you add a hop instead of removing one.

Does Convex replace Postgres? It replaces the database with its own document store. If you depend on Postgres features — row-level security, SQL functions, advisory locks, extensions — those don't come with you. This codebase's authorization boundary is six RLS policies, which is exactly the kind of thing that doesn't port.

How do I know which latency to optimise? Look at a real waterfall. On this site the answer was a 68 KiB JavaScript parse on every page, not a database round trip — and no amount of edge database would have found it. Database locality, bundle size and render-blocking work are three different problems, and only one of them is usually yours.

Templates in this post

ASoc Remit is a payments-platform marketing site with a transactions dashboard preview and a comparison pricing table — the product whose real dashboard is the authenticated, uncacheable read path discussed above. ASoc Script is an AI copywriting SaaS site with features, integrations, templates and pricing, a fully static marketing surface where database locality buys nothing. ASoc Seeker is an AI keyword-research SaaS landing page with a benefit grid, use cases and 2-tier pricing, the kind of app whose live query volume is the case for a reactive backend.

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

Keep reading

Comparison9 min read

CSS Modules vs. Sass: Two Classes Say Neither Is Needed Here

CSS Modules scopes class names; Sass preprocesses them — different jobs, often combined. This repo's entire hand-written stylesheet has two custom classes, and one is dead code.

Read more
Comparison8 min read

esbuild vs. Vite: What This Repo's Own Lockfile Says

Neither is a dependency here — but the Vite version vitest pulls in has already dropped esbuild for Rolldown, proven straight from the lockfile.

Read more
Comparison9 min read

Fathom vs. Vercel Analytics: Portability, Priced

Both are cookieless with a one-line install. The difference is what happens when you leave your host, measured against this site's live CSP, event wrapper and Lighthouse run.

Read more