Browser (WASM)
Dynoxide compiles to WebAssembly and runs in the browser. The same engine that backs the native build drives the official @sqlite.org/sqlite-wasm SQLite build, persists the database to OPFS (the browser's origin private file system), and runs the whole thing in a Web Worker. No server, no install, and nothing leaves the page.
It ships on npm as @dynoxide/wasm-engine. The package carries the engine .wasm, the SQLite .wasm, the bundled Worker, and an EngineClient that drives them, so you run real DynamoDB operations client-side with a few lines of JavaScript.
This is a preview. The wasm build isn't run against Parity Suite, the DynamoDB conformance suite that backs the native build, so treat its behaviour as illustrative rather than authoritative.
Want to see it without writing any code? accesspatterns.dev runs this same engine in the browser - guided lessons, real access-pattern models, and a freeform playground for learning DynamoDB by operating it.
Install
npm install @dynoxide/wasm-engine
That pulls the current preview. Pin the exact version (0.11.4-preview) if you'd rather lock to one and bump it deliberately. The npm version layers a -preview suffix on the engine's own crate version, so 0.11.4-preview is the wasm distribution of engine 0.11.4.
Quick start
import { EngineClient } from "@dynoxide/wasm-engine";
const client = new EngineClient();
await client.ready();
await client.execute("CreateTable", {
TableName: "Music",
KeySchema: [
{ AttributeName: "artist", KeyType: "HASH" },
{ AttributeName: "song", KeyType: "RANGE" },
],
AttributeDefinitions: [
{ AttributeName: "artist", AttributeType: "S" },
{ AttributeName: "song", AttributeType: "S" },
],
BillingMode: "PAY_PER_REQUEST",
});
await client.execute("PutItem", {
TableName: "Music",
Item: { artist: { S: "Pixies" }, song: { S: "Debaser" } },
});
const { Items } = await client.execute("Query", {
TableName: "Music",
KeyConditionExpression: "artist = :a",
ExpressionAttributeValues: { ":a": { S: "Pixies" } },
});
execute takes the operation name and a plain DynamoDB-JSON request, resolves with the response, and rejects with a typed EngineError. The client owns the Worker round-trip, so you deal in objects rather than postMessage envelopes. Calls issued before ready() resolves queue behind boot rather than racing it, so you can put work in flight straight after construction.
The client
new EngineClient(opts?)- boots the engine eagerly. Options are all optional:name(the database name and per-instance OPFS pool, defaultdynoxide.db),ephemeral(force an in-memory session), and the asset-resolution options below.await client.ready()- resolves with the boot descriptor:{ contractVersion, capabilities, persistenceMode }.client.execute(op, request)- run one operation. In TypeScript the response defaults tounknown, so pass a type argument when you have one:client.execute<ScanOutput>("Scan", request).client.supports(op)- whether the engine implements an operation, for capability-gating your UI.client.persistent-truewhen the session persists across reloads (OPFS),falsein the in-memory fallback.client.terminate()- tear the Worker down and reject any in-flight calls.
It also exports EngineError (the engine's __type lands on .type, so you branch on it instead of string-matching messages) and CONTRACT_VERSION. TypeScript declarations ship in the package, so the client and its boot descriptor are typed out of the box.
What works
Create-table, put, get, delete, query, scan and list-tables, over base tables and both secondary index types (GSI and LSI). UpdateTable adds or drops a GSI and changes the simple table settings. Index maintenance is atomic with the base write, same as the native build.
The engine reports its exact capability set at boot, so don't guess - gate on it. ready() hands you capabilities, and client.supports("TransactWriteItems") answers for a single op. Anything outside the set rejects with a typed EngineError rather than misbehaving: TTL returns an Unsupported error, streams aren't wired yet, and a handful of operations (tags, table stats, bulk import) stay preview placeholders. Branch on EngineError.type to tell a genuine rejection from a not-yet-wired one.
Where the assets live
The Worker and both .wasm travel inside the package. new EngineClient() with no arguments resolves the Worker next to the client module, and the Worker resolves its .wasm next to itself, so a bundler that copies the package's files - or a plain static deploy of them - needs no configuration.
| File | Size | What |
|---|---|---|
dynoxide_bg.wasm |
~1.0 MB | the engine |
sqlite3.wasm |
~845 KB | the official SQLite wasm build |
dynoxide-worker.js |
~225 KB | the bundled Web Worker |
About 2 MB of assets. They're immutable, cache well, and more than halve over the wire with gzip or brotli.
Serving from a CDN or a different origin? Two options on the constructor:
assetBase- the directory the assets sit in, e.g.new EngineClient({ assetBase: "https://cdn.example.com/dynoxide/" }).workerUrl- the exact Worker URL, if it doesn't sit beside its.wasmunder a shared base.
If you'd rather let a bundler construct the Worker, the package exposes it at the ./worker subpath. Build it and hand it back through createWorker:
new EngineClient({
createWorker: () =>
new Worker(new URL("@dynoxide/wasm-engine/worker", import.meta.url), { type: "module" }),
});
Two clients on one page are fine. Each gets its own storage pool keyed on name, so give them distinct names if both should persist.
Hosting
Put the assets on any origin that's a secure context - HTTPS in production, or localhost for development. OPFS needs a secure context, but no COOP/COEP headers and no cross-origin isolation. Serve the .wasm as application/wasm, and if you set a Content-Security-Policy it must allow 'wasm-unsafe-eval', or the engine won't instantiate.
That no-cross-origin-isolation part is the interesting bit. SQLite in the browser usually needs cross-origin isolation, because the common trick makes an async storage API look synchronous via SharedArrayBuffer. Dynoxide sidesteps that by running SQLite's synchronous OPFS VFS inside a Web Worker, where synchronous file handles are available directly. So it drops onto ordinary static hosting.
Persistence
State persists across reloads via OPFS. Where OPFS synchronous access handles aren't available - Firefox private windows, older Safari - the client falls back to an in-memory session and reports it through persistent / persistenceMode rather than failing. Check client.persistent if you need to tell the user their data won't survive a reload.
Versioning
CONTRACT_VERSION stamps the message-envelope shape, not the engine version. Adding an operation leaves it alone; changing a request, response or error envelope bumps it. The client validates it against the engine on boot and fails loudly on a mismatch, so a pinned consumer gets a clear error rather than mis-reading a newer engine. The shipped engine and contract versions sit in the package's manifest.json.
Building it yourself
You don't need to - the package above is the engine, prebuilt. If you want to build it from source, or you're after the harness that drives the shipping bundle as a worked example, the WebAssembly section of the README is the canonical reference.