Next.js + Vercel でプロトタイプを作るとき、Neon や Supabase のようなマネージドDBをすぐにセットアップするのはオーバーヘッドに感じることがあります。アイデア検証や社内ツールの初期段階では、アプリケーション内で完結するデータベース(インプロセスDB)で十分なケースが多いです。
この記事では、Vercel のサーバーレス制約を踏まえた上で、PGlite、sql.js、better-sqlite3、libSQL、DuckDB WASM、lowdb の6つを比較し、シナリオ別の選定指針を提示します。
Vercel のサーバーレス制約を理解する
まず前提として、Vercel のランタイム制約を押さえておく必要があります。
| 制約 | Node.js ランタイム | Edge ランタイム |
|---|---|---|
| ファイルシステム | 読み取り専用(/tmp のみ書き込み可、最大 500MB) | アクセス不可 |
/tmp の永続性 | ウォームインスタンス間のみ。コールドスタートで消失 | なし |
| ネイティブバイナリ | 動作する(Linux x64 向けビルド必要) | 動作しない |
| WASM | 対応 | 対応(gzip 後 1〜4MB のサイズ制限あり) |
| 最大実行時間 | 300 秒(Fluid Compute) | 30 秒 |
最も重要なポイントは、Vercel のサーバーレス関数にはサーバー側の永続ファイルシステムが存在しないということです。SQLite のようなファイルベースDBは書き込み先が保証されません。複数のインスタンスが起動すると、それぞれ独立した /tmp を持ち、データを共有できません。
つまり、Vercel でインプロセスDBを活用する場合、永続化の境界は**ブラウザ(クライアント側)か外部ストレージ(S3 など)**のどちらかになります。
永続化パターンの整理
Vercel 上でのインプロセスDB運用には、大きく3つのパターンがあります。
flowchart TD
A["DB が必要"] --> B{"永続書き込みが必要?"}
B -->|Yes| C{"保存先は?"}
C -->|"端末内"| D["IndexedDB / OPFS"]
C -->|"共有"| E["S3 / R2 同期"]
B -->|No| F{"実行環境は?"}
F -->|Browser| D
F -->|Node| G["/tmp, memory DB"]
F -->|Edge| H["メモリのみ"]
パターン1:ブラウザ内永続化(推奨)
データをユーザーの端末内に閉じて管理します。PGlite の IndexedDB モード、DuckDB WASM の OPFS、sql.js の手動エクスポートなどが該当します。サーバーコストゼロでオフライン動作も可能ですが、デバイス間のデータ共有はできません。
パターン2:読み取り専用の同梱DB
.sqlite ファイルをデプロイに含め、サーバー側で参照のみ行います。データ更新は再デプロイで対応。辞書や参照データの配信に適しています。
パターン3:一時DB(デモ・検証用)
/tmp やメモリ上にDBを作成します。コールドスタートで消失することを前提とした、デモやE2Eテスト向けの構成です。
6つのインプロセスDBを比較する
PGlite:ブラウザで PostgreSQL を動かす
PGlite は PostgreSQL 17 を WebAssembly にコンパイルしたもので、gzip 後約 3MB のフットプリントで動作します。トランザクション、CTE、ウィンドウ関数、JSONB、さらには pgvector による類似検索まで、PostgreSQL のフル機能が利用できます。
// app/db/client-db.ts(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 が drizzle-orm/pglite でファーストクラスサポートしており、型安全なクエリが書けます。React 向けには @electric-sql/pglite-react の useLiveQuery フックでリアクティブなデータ取得も可能です。
ただし、コールドスタート時の初期化に 500ms〜2秒かかる点と、単一接続しかサポートしない点には注意が必要です。Edge ランタイムではサイズ制限によりほぼ動作しません。
向いているケース: 将来 Neon や Supabase に移行予定のプロトタイプ、ブラウザ完結のローカルファーストアプリ
sql.js:最軽量の SQL エンジン
sql.js は SQLite を Emscripten で WASM 化したもので、約 1MB と最小サイズです。コールドスタートも 50〜200ms と高速で、Vercel 上で最もトラブルが少ない WASM DB です。
// 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;
}DB 全体がメモリ上に展開される設計のため、大きなDBには不向きです。永続化は db.export() で Uint8Array を取り出し、IndexedDB に手動保存する形になります。Drizzle、Prisma、Kysely のドライバが存在しないため、SQL を直接書く必要があります。
向いているケース: コールドスタート速度を重視するプロトタイプ、同梱 SQLite ファイルの読み取り専用配信
better-sqlite3:ローカル開発最速のネイティブバインディング
better-sqlite3 は Node.js 向けの C++ ネイティブアドオンで、同期 API による圧倒的な速度が特徴です。Drizzle ORM、Prisma、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 });
}ただし、Vercel へのデプロイでは Linux x64 向けのネイティブバイナリが必要で、ビルド互換性の問題が発生しやすいです。Edge ランタイムやブラウザでは動作しません。書き込みは /tmp に限定され、永続化はできません。
向いているケース: 読み取り専用の同梱DBによる高速検索(辞書、カタログ)、ローカル開発環境
libSQL:プロトタイプから本番への最短パス
Turso が開発する SQLite フォーク libSQL で、@libsql/client を通じて3つの接続モードを提供します。ローカル開発では file:local.db を使い、デプロイ時に libsql://your-db.turso.io に URL を切り替えるだけで本番に移行できます。
// lib/db.ts
import { createClient } from '@libsql/client';
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!, // ローカル: file:local.db
authToken: process.env.TURSO_AUTH_TOKEN, // ローカルでは不要
});2026年2月4日にリリースされた @tursodatabase/vercel-experimental は、Turso の部分同期をVercel Functions で実現します。コールドスタート時にスキーマと頻出ページのみ(128KiB)をダウンロードし、追加ページは /tmp にキャッシュ、書き込みはリモートの Turso に送る仕組みです。
Drizzle ORM の drizzle-orm/libsql と Prisma の @prisma/adapter-libsql の両方がサポートされています。ただし、Edge ランタイムでは file: プロトコルが使えないため、HTTP/WSS 経由のリモート接続が前提となります。
向いているケース: SQLite で始めて Turso に移行する予定のプロジェクト、URL 変更だけでデプロイしたいチーム
DuckDB WASM:ブラウザで分析SQLを実行する
DuckDB WASM は列指向ストレージとベクトル化クエリエンジンを備えた分析特化型DBです。S3 や R2 に置いた Parquet ファイルをブラウザから直接 SQL でクエリできるのが最大の特徴です。
SELECT region, SUM(revenue) as total
FROM 'https://storage.example.com/sales.parquet'
GROUP BY region
ORDER BY total DESCブラウザの OPFS を使えば永続化も可能です。ただし、Next.js のサーバーサイドでは動作しません。Web Worker 依存のアーキテクチャが App Router と衝突し、ハングやワーカー終了エラーが発生します。MotherDuck の公式 Vercel 連携もクライアントサイド専用です。
また、2025年9月にサプライチェーン攻撃(CVE-2025-59037)が発生しており、npm パッケージ v1.29.2 が侵害されました。v1.30.0 以降を使用してください。
向いているケース: ブラウザ上の分析ダッシュボード、Parquet/CSV を直接クエリする可視化ツール
lowdb:JSON ファイルによる最小構成
lowdb は JSON をそのままデータストアとして扱うミニマルなライブラリです。ブラウザでは localStorage、Node.js では JSON ファイルをアダプタとして使います。
// 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;
}SQL の知識不要で最短導入できますが、インデックスや結合、複雑なクエリは期待できません。Vercel のサーバーサイドでは JSON ファイルの永続書き込みができないため、ブラウザ内利用か一時用途に限定されます。Node の cluster もサポートしていません。
向いているケース: 設定値や下書きの保存、SQL 不要な超軽量プロトタイプ
比較表
| DB | Vercel サーバーレス | コールドスタート | Drizzle ORM | Vercel 上の永続化 | サイズ | SQL 方言 |
|---|---|---|---|---|---|---|
| PGlite | 動作(設定必要) | 500ms〜2s | ファーストクラス | ブラウザ IndexedDB のみ | ~3MB gz | PostgreSQL |
| sql.js | 良好 | 50〜200ms | 非対応 | 手動エクスポート / 読み取り専用同梱 | ~1MB | SQLite |
| better-sqlite3 | ネイティブバイナリ問題あり | ~10ms | ファーストクラス | 読み取り専用同梱のみ | ~10MB native | SQLite |
| libSQL | HTTP 経由で良好 | 低〜中 | ファーストクラス | Turso 部分同期で可能 | 可変 | SQLite + 拡張 |
| DuckDB WASM | サーバー側で動作不可 | 1〜5s | コミュニティのみ | クライアント側 OPFS | ~3.2MB gz | DuckDB(PG 風) |
| lowdb | 読み取り専用 FS で書き込み不可 | 即時 | 非対応 | ブラウザ localStorage | ~10KB | JSON / JS 配列 |
シナリオ別の選定ガイド
将来 PostgreSQL に移行するプロトタイプ → PGlite + Drizzle ORM
ブラウザの IndexedDB 永続化でインフラ設定ゼロで始められます。スキーマとクエリは Neon や Supabase にそのまま移行可能です。コールドスタートが気になる場合はクライアントサイドで動かしましょう。
ローカル開発からデプロイまで最短で → libSQL + Drizzle ORM
file:local.db で開発し、デプロイ時に URL を libsql://your-db.turso.io に変えるだけ。@tursodatabase/vercel-experimental の部分同期を使えば、ローカル速度の読み取りと耐久性のある書き込みが両立します。
ブラウザ完結のクライアントアプリ → Dexie.js / TinyBase
IndexedDB ベースの永続化でサーバー不要です。Dexie.js は useLiveQuery() によるリアクティブなデータ取得、TinyBase は 3.5〜8.7kB のフットプリントで深いリアクティビティを提供します。フル SQL が必要なら PGlite のブラウザモード(idb://)を選択してください。
読み取り専用の参照データ配信 → sql.js
.sqlite ファイルをデプロイに同梱し、50〜200ms の初期化で軽量にクエリできます。ネイティブバイナリの問題もありません。データ更新は再デプロイで対応します。
分析ダッシュボードの構築 → DuckDB WASM(クライアントサイド)
S3/R2 の Parquet ファイルをブラウザから直接クエリでき、サーバーの計算リソースを消費しません。必ずクライアントサイドで使い、サーバーサイドでの利用は避けてください。
避けるべき選択肢
- LokiJS:2022年以降メンテナンスされておらず、クエリ結果の不整合やデータ消失のバグがあります。公式サイトは RxDB にリダイレクトされています
- cr-sqlite:事実上メンテナンスが停止しており、47件のオープンイシューが残っています
- DuckDB WASM のサーバーサイド利用:App Router との互換性問題が未解決です
まとめ
Vercel のサーバーレス環境における永続化の境界は明確です。関数のファイルシステムは永続ストレージではなく、データはブラウザかクラウドに置く必要があります。
この制約を受け入れた上で、PGlite はブラウザ内での PostgreSQL 体験を、libSQL/Turso は「URL を変えるだけ」のデプロイモデルを提供します。2026年2月4日リリースの Turso 部分同期パッケージは、サーバーレスにリモート書き込みターゲットが必要という現実に正面から向き合った、最もアーキテクチャ的に誠実なソリューションです。
プロトタイプの性質と成長予測に応じて、この記事の選定ガイドを参考に最適な選択を行ってください。
以上、Vercel + Next.js でマネージドDBなしのプロトタイピングに挑んだ、現場からお送りしました。