Skip to content

DuckDB

The experimental @opfs-vfs/opfs-vfs/duckdb export connects DuckDB-Wasm’s synchronous filesystem callbacks to OPFS VFS. Database and WAL files live inside your mounted volume; SQL queries run in a dedicated browser worker.

The website hosts a prebuilt compatible engine. OPFS VFS, its website, and its CI do not compile DuckDB. The stock DuckDB-Wasm package is not compatible with this adapter because the required filesystem callbacks and direct I/O behavior need source changes.

Try the DuckDB demo to run SQL, save tables, and reopen them from an OPFS VFS volume.

Use the matching files from custom asset release 1.33.1-dev64.0-opfs-vfs.1:

Download the JavaScript and Wasm together and serve them with your application. Keep the notices when redistributing them. These are custom EH assets, not an official DuckDB release or files included in the OPFS VFS npm package. Install @duckdb/duckdb-wasm@1.33.1-dev64.0 for the declaration types. See Connect the engine to OPFS VFS for the worker setup.

The versioned directory keeps this pair stable. Publish an upgraded pair at a new versioned path after testing it. Checksums identify these artifacts; they do not promise byte-for-byte reproduction on every build machine.

The currently tested source is OPFS VFS’s DuckDB-Wasm fork, commit 12651a2d772ce17d85466c3ba0929f867aeec814. It includes the filesystem changes on top of upstream 1.33.1-dev64.0. The integration is still a draft; arbitrary upstream releases are not claimed compatible.

Building is optional if you use the downloads above. The commands below run in a separate DuckDB-Wasm checkout, outside your application and the OPFS VFS repository. You need Git, Make, Python with venv support, Node.js, Emscripten 3.1.71, and CMake 3.31.6. The initial C++ compilation needs several GB of disk space and can take tens of minutes. The verified build uses the EH variant, without loadable extensions.

Start in a directory where you keep external source checkouts. Use a fresh checkout for these commands; the patch step is intended to run once.

Terminal window
set -eu
git clone https://github.com/opfs-vfs/duckdb-wasm.git duckdb-wasm-opfs
cd duckdb-wasm-opfs
git checkout --detach 12651a2d772ce17d85466c3ba0929f867aeec814
git submodule update --init --recursive
# These are the fork's existing upstream dependency patches.
for component in duckdb arrow rapidjson; do
for patch in "$PWD/patches/$component/"*.patch; do
git -C "submodules/$component" apply --check "$patch"
git -C "submodules/$component" apply "$patch"
done
done
PUPPETEER_SKIP_DOWNLOAD=1 npx --yes yarn@1.22.22 install --frozen-lockfile
git clone --branch 3.1.71 --depth 1 https://github.com/emscripten-core/emsdk.git .emsdk
test "$(git -C .emsdk rev-parse HEAD)" = 59108b0e222d4c38530a5f2bebcecc7db965f52d
./.emsdk/emsdk install 3.1.71
./.emsdk/emsdk activate 3.1.71
. ./.emsdk/emsdk_env.sh
python3 -m venv .build-tools
.build-tools/bin/pip install cmake==3.31.6
export PATH="$PWD/.build-tools/bin:$PATH"
unset DUCKDB_WASM_LOADABLE_EXTENSIONS

Keep the three-level build directory below. Upstream’s export list uses relative paths.

Terminal window
emcmake cmake -S lib -B build/relperf/eh-make -G 'Unix Makefiles' \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_WASM_EXCEPTIONS=1 \
-DDUCKDB_EXPLICIT_PLATFORM=wasm_eh \
-DDUCKDB_WASM_VERSION=1.33.1-dev64.0 \
-DCMAKE_C_COMPILER_LAUNCHER= \
-DCMAKE_CXX_COMPILER_LAUNCHER=
cmake --build build/relperf/eh-make --target duckdb_wasm --parallel 4

Still in the DuckDB-Wasm checkout, bundle the blocking browser bindings with the JavaScript generated by that build. This uses the fork’s installed esbuild dependency. It does not edit generated JavaScript or Wasm.

Terminal window
node --input-type=module <<'JS'
import { build } from 'esbuild';
import { copyFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
const output = resolve('opfs-vfs-assets');
const binary = resolve('build/relperf/eh-make');
mkdirSync(output, { recursive: true });
await build({
stdin: {
contents: `export { DuckDB } from './bindings/bindings_browser_eh';
export { VoidLogger } from './log';
export { DuckDBAccessMode } from './bindings/config';`,
resolveDir: resolve('packages/duckdb-wasm/src'),
},
bundle: true,
format: 'esm',
platform: 'browser',
target: 'es2022',
minify: true,
outfile: `${output}/duckdb.js`,
plugins: [{
name: 'matched-browser-engine',
setup(builder) {
builder.onResolve({ filter: /^\.\/duckdb-eh\.js$/ }, () => ({ path: `${binary}/duckdb_wasm.js` }));
// Emscripten's Node branches are unreachable in a browser worker.
builder.onResolve({ filter: /^(fs|path|crypto)$/ }, args => ({ path: args.path, namespace: 'browser-only' }));
builder.onLoad({ filter: /.*/, namespace: 'browser-only' }, () => ({
contents: 'throw new Error("This DuckDB build requires a browser worker");',
}));
},
}],
});
copyFileSync(`${binary}/duckdb_wasm.wasm`, `${output}/duckdb.wasm`);
writeFileSync(`${output}/duckdb.d.ts`, `import type { DuckDBBindings, DuckDBRuntime, Logger } from '@duckdb/duckdb-wasm/blocking';
export { VoidLogger, DuckDBAccessMode } from '@duckdb/duckdb-wasm/blocking';
export declare const DuckDB: new (logger: Logger, runtime: DuckDBRuntime, wasmURL: string) => DuckDBBindings;
`);
JS

The opfs-vfs-assets directory now contains duckdb.js, duckdb.wasm, and TypeScript declarations. Keep the JavaScript and Wasm pair together. Do not substitute a stock npm or CDN binary for either file. Preserve DuckDB-Wasm and dependency license notices when distributing the assets. Install @duckdb/duckdb-wasm@1.33.1-dev64.0 in your application for the declaration types.

Serve or bundle those assets with your application. In this example, ./duckdb.js is the externally built browser bundle and /duckdb.wasm is its matching Wasm URL. Run this code in a dedicated browser worker. Your server must use HTTPS or localhost and send Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp for SharedArrayBuffer.

import { OpfsVfs } from '@opfs-vfs/opfs-vfs';
import { createDuckDBRuntime } from '@opfs-vfs/opfs-vfs/duckdb';
import { DuckDB, VoidLogger, DuckDBAccessMode } from './duckdb.js';
const vfs = new OpfsVfs('analytics.bin', {
bufferMode: 'disk',
localDurabilityMode: 'strict',
});
await vfs.ready;
const db = new DuckDB(new VoidLogger(), createDuckDBRuntime(vfs), '/duckdb.wasm');
await db.instantiate(() => {});
db.open({ path: '/analytics.db', accessMode: DuckDBAccessMode.READ_WRITE, useDirectIO: true });
const connection = db.connect();
connection.query('CREATE TABLE IF NOT EXISTS sales (amount INTEGER)');
connection.query('INSERT INTO sales VALUES (42)');
const rows = connection.query('SELECT sum(amount) FROM sales').toArray();
// On orderly shutdown:
connection.query('CHECKPOINT');
connection.close();
db.reset();
await vfs.closeVfs();

Use one database runtime per worker and ordinary local file paths such as /analytics.db. useDirectIO: true is required for database and WAL writes to reach the VFS sync barriers. The fork exposes _duckdb_web_opfs_vfs_abi() as a compatibility marker; the adapter rejects engines without it or with buffering enabled. No manual WAL deletion, file registration, or application flush after each commit is needed.

The volume lock prevents simultaneous mounts. Wait for shutdown before remounting from another worker. After a failed commit, the outcome may be uncertain; reopen and inspect the database before retrying.

The repository integration guide explains how to use the checked-in prebuilt assets or your own build with the browser example and run test:duckdb. Normal OPFS VFS tests and CI do not run this optional suite.

Its ten tests cover SQL results, rollback, read-only access, terminated-worker recovery, repeated reopen/write/checkpoint cycles, literal filenames, incompatible engines, and injected storage failures. Run it whenever you change the engine or adapter. Rebuild into a fresh output directory, keep the previous matching asset pair until validation passes, and review source/API changes before upgrading.

These tests do not establish power-loss durability on every browser or device. Remote protocols, wildcard globs, directory listing, JavaScript UDFs, loadable extensions, and the stock AsyncDuckDB worker are outside this adapter’s scope. The adapter depends on an unstable upstream runtime interface. For an adapter that needs no custom engine build, see PGlite.