Prototyping on Vercel + Next.js Without an External DB: Comparing 6 Embedded Databases
When building prototypes with Next.js on Vercel, spinning up a managed database like Neon or Supabase can feel like unnecessary overhead. For idea validation or early-stage internal tools, an in-process database that runs entirely within your application is often sufficient.
This article compares six embedded database options — PGlite, sql.js, better-sqlite3, libSQL, DuckDB WASM, and lowdb — against Vercel’s serverless constraints, and provides scenario-based selection guidance.
Understanding Vercel’s Serverless Constraints
Before evaluating any database, you need to understand the hard boundaries.
| Constraint | Node.js Runtime | Edge Runtime |
|---|---|---|
| Filesystem | Read-only (/tmp writable, max 500MB) | No access |
/tmp persistence | Warm instances only. Lost on cold start | None |
| Native binaries | Supported (Linux x64 build required) | Not supported |
| WASM | Supported | Supported (1–4MB gzipped size limit) |
| Max execution time | 300s (Fluid Compute) | 30s |
The critical takeaway: Vercel serverless functions have no persistent server-side filesystem. File-based databases like SQLite have no guaranteed write destination. When multiple instances spin up, each gets an independent /tmp with no shared state.
This means persistence on Vercel must live either in the browser (client-side) or in external storage (S3, etc.).
Persistence Patterns on Vercel
There are three main patterns for running in-process databases on Vercel.
flowchart TD
A["Need a DB"] --> B{"Persistent writes?"}
B -->|Yes| C{"Where to store?"}
C -->|"User device"| D["IndexedDB / OPFS"]
C -->|"Shared"| E["S3 / R2 sync"]
B -->|No| F{"Execution env?"}
F -->|Browser| D
F -->|Node| G["/tmp or in-memory DB"]
F -->|Edge| H["In-memory only"]
Pattern 1: Browser-side persistence (recommended)
Data lives entirely on the user’s device. PGlite with IndexedDB, DuckDB WASM with OPFS, or sql.js with manual export. Zero server cost, offline-capable, but no cross-device data sharing.
Pattern 2: Read-only bundled database
Ship a .sqlite file with your deployment and query it server-side. Update data by redeploying. Great for dictionaries, catalogs, and reference data.
Pattern 3: Ephemeral database (demos/testing)
Create a database in /tmp or memory. It will be lost on cold start — suitable only for demos and E2E tests.
Comparing 6 Embedded Databases
PGlite: Full PostgreSQL in the Browser
PGlite compiles PostgreSQL 17 to WebAssembly, running at ~3MB gzipped. You get the full PostgreSQL feature set — transactions, CTEs, window functions, JSONB, and even pgvector for similarity search.
// app/db/client-db.ts (called from Client Component)
import { PGlite } from '@electric-sql/pglite';
export async function openDb() {
const db = new PGlite('idb://my-app-db');
await db.exec(`
CREATE TABLE IF NOT EXISTS todo (
id SERIAL PRIMARY KEY,
task TEXT NOT NULL,
done BOOLEAN DEFAULT FALSE
)
`);
return db;
}Drizzle ORM provides first-class support via drizzle-orm/pglite, and @electric-sql/pglite-react offers useLiveQuery hooks for reactive data fetching.
Watch out for 500ms–2s cold start initialization and the single-connection limitation. It won’t run on Edge Runtime due to size constraints.
Best for: Prototypes targeting PostgreSQL in production (Neon, Supabase), browser-based local-first apps.
sql.js: The Lightest SQL Engine
sql.js compiles SQLite to WASM via Emscripten at ~1MB — the smallest footprint in this comparison. Cold start initialization takes just 50–200ms, making it the least problematic WASM database on Vercel.
// app/db/sqljs.ts
import initSqlJs from 'sql.js';
export async function openSqlJs() {
const SQL = await initSqlJs({
locateFile: (file) => `https://sql.js.org/dist/${file}`,
});
const db = new SQL.Database();
db.run(`CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)`);
return db;
}The entire database lives in memory, so large databases are problematic. Persistence requires manually exporting via db.export() to a Uint8Array and saving to IndexedDB. No Drizzle, Prisma, or Kysely driver exists — you’ll write SQL directly.
Best for: Fast cold starts, bundled read-only SQLite files, projects where ORM support isn’t critical.
better-sqlite3: Fastest Native Bindings for Local Development
better-sqlite3 is a C++ native addon for Node.js with synchronous APIs and exceptional performance. First-class support from Drizzle ORM, Prisma, and Kysely.
// app/api/search/route.ts
export const runtime = 'nodejs';
import Database from 'better-sqlite3';
const db = new Database('data/catalog.sqlite', { readonly: true });
export async function GET(req: Request) {
const q = new URL(req.url).searchParams.get('q') ?? '';
const rows = db
.prepare('SELECT * FROM items WHERE name LIKE ? LIMIT 20')
.all(`%${q}%`);
return Response.json({ rows });
}Deploying to Vercel requires a Linux x64 native binary, which often causes compatibility issues. Doesn’t work on Edge Runtime or in browsers. Writes are limited to ephemeral /tmp.
Best for: Read-only bundled databases for fast search (dictionaries, catalogs), local development.
libSQL: The Smoothest Path from Prototype to Production
Turso’s SQLite fork libSQL provides three connection modes via @libsql/client. Develop locally with file:local.db, then change the URL to libsql://your-db.turso.io for production — no code changes needed.
// lib/db.ts
import { createClient } from '@libsql/client';
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!, // local: file:local.db
authToken: process.env.TURSO_AUTH_TOKEN, // not needed locally
});The @tursodatabase/vercel-experimental package (first released February 4, 2026) brings partial sync to Vercel Functions — downloading only the first 128KiB of schema and hot pages on cold start, caching in /tmp, and routing writes to the remote Turso instance.
Both Drizzle ORM (drizzle-orm/libsql) and Prisma (@prisma/adapter-libsql) are supported. Note that file: protocol doesn’t work on Edge Runtime — HTTP/WSS remote connections are required there.
Best for: Starting with SQLite and migrating to Turso, teams that want the “change one URL” deployment model.
DuckDB WASM: Analytical SQL in the Browser
DuckDB WASM is a columnar, vectorized query engine optimized for analytics. Its killer feature is querying remote Parquet files directly from the browser via SQL.
SELECT region, SUM(revenue) as total
FROM 'https://storage.example.com/sales.parquet'
GROUP BY region
ORDER BY total DESCPersistence is available via browser OPFS. However, DuckDB WASM is broken in Next.js server-side code. Its Web Worker architecture conflicts with App Router, causing hangs and worker termination errors. MotherDuck’s official Vercel integration is client-side only.
Also note: a supply chain attack (CVE-2025-59037) compromised npm packages including v1.29.2 in September 2025. Use v1.30.0 or later.
Best for: Client-side analytics dashboards, querying Parquet/CSV files in the browser.
lowdb: Minimal JSON Storage
lowdb is a minimalist library that treats JSON as a data store. Uses localStorage in browsers and JSON files in Node.js.
// app/db/lowdb.ts
import { LowSync } from 'lowdb';
import { LocalStorage } from 'lowdb/browser';
type Data = { drafts: { id: string; text: string }[] };
export function openLowdb() {
const adapter = new LocalStorage<Data>('myapp');
const db = new LowSync<Data>(adapter, { drafts: [] });
db.read();
return db;
}Zero SQL knowledge required for the fastest possible setup, but no indexes, joins, or complex queries. Server-side JSON file writes don’t persist on Vercel, so it’s limited to browser usage or ephemeral scenarios. Doesn’t support Node’s cluster mode.
Best for: Saving settings or drafts, ultra-lightweight prototypes that don’t need SQL.
Comparison Table
| DB | Vercel Serverless | Cold Start | Drizzle ORM | Persistence on Vercel | Size | SQL Dialect |
|---|---|---|---|---|---|---|
| PGlite | Works (config needed) | 500ms–2s | First-class | Browser IndexedDB only | ~3MB gz | PostgreSQL |
| sql.js | Works well | 50–200ms | No driver | Manual export / read-only bundle | ~1MB | SQLite |
| better-sqlite3 | Native binary issues | ~10ms | First-class | Read-only bundle only | ~10MB native | SQLite |
| libSQL | Good via HTTP | Low–Medium | First-class | Turso partial sync | Varies | SQLite + extensions |
| DuckDB WASM | Broken server-side | 1–5s | Community only | Client-side OPFS | ~3.2MB gz | DuckDB (PG-like) |
| lowdb | Cannot write (read-only FS) | Instant | No driver | Browser localStorage | ~10KB | JSON / JS arrays |
Scenario-Based Selection Guide
Prototype heading to PostgreSQL → PGlite + Drizzle ORM
Start with zero infrastructure using browser IndexedDB persistence. Schemas and queries transfer directly to Neon or Supabase. Run client-side if cold start times are a concern.
Fastest path from local dev to deployment → libSQL + Drizzle ORM
Develop with file:local.db, deploy by changing the URL to libsql://your-db.turso.io. The @tursodatabase/vercel-experimental partial sync gives you local-speed reads with durable cloud writes.
Browser-only client app → Dexie.js / TinyBase
IndexedDB-based persistence with no server needed. Dexie.js provides reactive data via useLiveQuery(), TinyBase offers deep reactivity at 3.5–8.7kB. Choose PGlite in browser mode (idb://) if you need full SQL client-side.
Read-only reference data → sql.js
Bundle a .sqlite file with your deployment and query it with 50–200ms initialization. No native binary concerns. Update data by redeploying.
Analytics dashboards → DuckDB WASM (client-side)
Query Parquet files on S3/R2 directly from the browser without server compute costs. Always use client-side — never attempt server-side DuckDB on Next.js.
What to Avoid
- LokiJS: Unmaintained since 2022 with known bugs including wrong query results and data loss. Official site redirects to RxDB
- cr-sqlite: Effectively unmaintained with 47 open issues
- DuckDB WASM server-side: Unresolved App Router compatibility issues
Conclusion
The persistence boundary on Vercel serverless is clear: the function’s filesystem is not persistent storage — data must live in the browser or the cloud.
Within this constraint, PGlite brings genuine PostgreSQL to browser-based prototyping, while libSQL/Turso uniquely solves the “works locally, persists in production” problem with a single URL change. The Turso partial-sync package (released February 4, 2026) is the most architecturally honest solution to Vercel’s ephemeral filesystem — acknowledging that serverless needs a remote write target while optimizing reads locally.
Choose based on your prototype’s nature and growth trajectory, using this guide to find the right fit.
That’s all from the Gemba, where I explored embedded database options for prototyping on Vercel + Next.js without a managed DB.