Deno Desktop

Setup

Serve and update Webview Bundle archives in a Deno desktop webview through a Deno.serve request handler.

Experimental

Deno Desktop renders a native webview with Deno.BrowserWindow and serves it over Deno.serve. Webview Bundle becomes that server's handler, so the window loads your packed .wvb assets offline instead of fetching them over the network.

main.ts
import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop';

const wvb = await webviewBundle({
  protocols: [bundleProtocol('app')],
});

const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });
await win.closed;

@wvb/deno-desktop is the integration layer; @wvb/deno is the FFI peer of @wvb/node that drives the shared Rust core.

How it fits together

The window points at a single local origin served by Deno.serve, so the integration is single-origin: it allows exactly one protocol. See Platform integration for how the core reaches each platform and Protocol handling for the protocol model.

@wvb/deno-desktop

Call webviewBundle(config) — aliased as wvb, backed by the WebviewBundle class — to build a BundleSource (plus an optional Remote and Updater) and get back a Deno.serve-compatible fetch handler. Add an updater to pull newer bundles from a remote:

main.ts
import { webviewBundle, bundleProtocol } from '@wvb/deno-desktop';

const wvb = await webviewBundle({
  protocols: [bundleProtocol('app')],
  updater: {
    remote: { endpoint: 'https://bundles.example.com' },
  },
});

const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });
await win.closed;

Bindings

Call registerBindings(win, wvb) to expose native commands to your web app. It registers one Deno.BrowserWindow binding named wvbInvoke, reachable from the page as window.bindings.wvbInvoke, that dispatches every @wvb/bridge command in the source.*, remote.*, and updater.* groups.

main.ts
import { webviewBundle, bundleProtocol, registerBindings } from '@wvb/deno-desktop';

const wvb = await webviewBundle({
  protocols: [bundleProtocol('app')],
});

const server = Deno.serve(wvb.fetch);
const win = new Deno.BrowserWindow({ url: `http://localhost:${server.addr.port}` });

registerBindings(win, wvb); // wires window.bindings.wvbInvoke

await win.closed;

The binding never throws across the FFI boundary. Each call resolves to an InvokeResult envelope:

type InvokeResult =
  | { ok: true; value: unknown }
  | { ok: false; error: { code?: string; message: string } };

The @wvb/bridge client unwraps this envelope once it detects the deno platform, so web code keeps calling invoke(), source.*, remote.*, and updater.* exactly as on other platforms:

app.ts (in the webview)
import { updater } from '@wvb/bridge';

const update = await updater.getUpdate('my-app');
if (update.isAvailable) {
  await updater.download('my-app');
  await updater.install('my-app', update.version);
}

@wvb/deno

@wvb/deno is the FFI peer of @wvb/node. It loads a prebuilt Rust cdylib through Deno.dlopen and exposes the same core classes: BundleProtocol, LocalProtocol, Remote, BundleSource, and Updater.

Each class is Disposable — free it explicitly with free(), or let a using declaration call [Symbol.dispose] at scope exit:

dispose.ts
import { BundleProtocol } from '@wvb/deno';

// `lib` and `source` come from the load + BundleSource steps below.
// Explicit cleanup.
const protocol = new BundleProtocol(lib, source);
const res = await protocol.handle('get', 'app://my-app/index.html');
protocol.free();

// Or scope-bound cleanup with `using`.
{
  using scoped = new BundleProtocol(lib, source);
  await scoped.handle('get', 'app://my-app/index.html');
} // [Symbol.dispose]() runs here

Load the native library

Load the cdylib from an explicit path, or download a SHA-256-verified prebuilt via @denosaurs/plug:

load.ts
import { loadLib, loadLibViaPlug } from '@wvb/deno';

// 1. Load a library already on disk.
const lib = loadLib('./vendor/wvb/libwvb_deno.dylib');

// 2. Download a sha256-verified prebuilt at runtime.
const libViaPlug = await loadLibViaPlug();

Point loadLib at a file with the WVB_DENO_LIB environment variable instead of hard-coding the path:

WVB_DENO_LIB=./vendor/wvb/libwvb_deno.dylib deno run -A main.ts

Or vendor the library ahead of time with the installer subcommand:

deno run -A jsr:@wvb/deno/install --out vendor/wvb

@wvb/deno/install downloads the cdylib from GitHub Releases and verifies its SHA-256 checksum by default. Supported targets:

Target triple
aarch64-apple-darwin
x86_64-apple-darwin
aarch64-unknown-linux-gnu
x86_64-unknown-linux-gnu
x86_64-pc-windows-msvc

Vendor a specific target with --target:

deno run -A jsr:@wvb/deno/install --out vendor/wvb --target x86_64-unknown-linux-gnu

For the full class and method reference, see the Deno API reference.

Limitations

Two gaps apply to the Deno bindings today.

Custom verifier callbacks are not supported. The Updater accepts only the declarative signatureVerifier — a SignatureVerifierOptions with an algorithm and a key:

updater.ts
const wvb = await webviewBundle({
  protocols: [bundleProtocol('app')],
  updater: {
    remote: { endpoint: 'https://bundles.example.com' },
    integrityPolicy: 'strict',
    signatureVerifier: {
      algorithm: 'ed25519',
      key: { format: 'raw', data: publicKeyBytes },
    },
  },
});

The custom integrityChecker / signatureVerifier function callbacks available in @wvb/node cannot cross the FFI boundary.

HttpOptions.defaultHeaders is not supported on the Deno Remote. Other HttpOptions fields still apply:

http.ts
const wvb = await webviewBundle({
  protocols: [bundleProtocol('app')],
  updater: {
    remote: {
      endpoint: 'https://bundles.example.com',
      http: { userAgent: 'my-app/1.0', timeout: 120_000 },
    },
  },
});

For how integrity and signatures work across platforms, see Remote bundles.

On this page