# DuckDB

> Set up the experimental DuckDB adapter with a compatible custom DuckDB-Wasm engine.

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](/demos/duckdb/) to run SQL, save tables, and reopen them from an OPFS VFS volume.

## Download the compatible build

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

- [Browser JavaScript bundle](/vendor/duckdb/1.33.1-dev64.0-opfs-vfs.1/duckdb.js), about 241 KB.
- [WebAssembly engine](/vendor/duckdb/1.33.1-dev64.0-opfs-vfs.1/duckdb.wasm), about 24 MB.
- <a href="/vendor/duckdb/1.33.1-dev64.0-opfs-vfs.1/duckdb.d.ts" download>TypeScript declarations</a>.
- [Build provenance and SHA-256 checksums](/vendor/duckdb/1.33.1-dev64.0-opfs-vfs.1/build.json).
- [Third-party licenses and notices](/vendor/duckdb/1.33.1-dev64.0-opfs-vfs.1/THIRD_PARTY_NOTICES.txt).

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](#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.

## Build your own compatible engine

The currently tested source is [OPFS VFS's DuckDB-Wasm fork](https://github.com/opfs-vfs/duckdb-wasm/pull/1), 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.

### Prepare the source and compiler

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.

```sh
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
```

### Compile the EH engine

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

```sh
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
```

### Bundle matching browser bindings

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.

```sh
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.

## Connect the engine to OPFS VFS

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.

```ts
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.

## Validate an engine build

The [repository integration guide](https://github.com/opfs-vfs/opfs-vfs/blob/main/docs/DUCKDB.md) 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](/docs/integrations/pglite/).
