Prototyping on Vercel + Next.js Without an External DB: Comparing 6 Embedded Databases

Tadashi Shigeoka ·  Mon, February 9, 2026

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.

ConstraintNode.js RuntimeEdge Runtime
FilesystemRead-only (/tmp writable, max 500MB)No access
/tmp persistenceWarm instances only. Lost on cold startNone
Native binariesSupported (Linux x64 build required)Not supported
WASMSupportedSupported (1–4MB gzipped size limit)
Max execution time300s (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 DESC

Persistence 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

DBVercel ServerlessCold StartDrizzle ORMPersistence on VercelSizeSQL Dialect
PGliteWorks (config needed)500ms–2sFirst-classBrowser IndexedDB only~3MB gzPostgreSQL
sql.jsWorks well50–200msNo driverManual export / read-only bundle~1MBSQLite
better-sqlite3Native binary issues~10msFirst-classRead-only bundle only~10MB nativeSQLite
libSQLGood via HTTPLow–MediumFirst-classTurso partial syncVariesSQLite + extensions
DuckDB WASMBroken server-side1–5sCommunity onlyClient-side OPFS~3.2MB gzDuckDB (PG-like)
lowdbCannot write (read-only FS)InstantNo driverBrowser localStorage~10KBJSON / 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.