---
title: "Dynoxide 0.10.0: it runs in the browser now - Martin Hicks"
description: "Dynoxide 0.10.0 compiles to WebAssembly and runs in the browser on OPFS, ships an official ~5 MB Docker image, and adds a storage-backend trait. Plus what breaks."
canonical: https://martinhicks.dev/articles/dynoxide-0100-browser-and-docker
last-updated: 2026-07-01
---

![Illustration of the Dynoxide logo with DynamoDB table icons on a dark background](https://martinhicks.dev/images/articles/dynoxide-0100-browser-and-docker.png)29th May 2026

# Dynoxide 0.10.0: it runs in the browser now

A DynamoDB-compatible database, running in a browser tab. No server, no network. The data lives in the [origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system), and the query engine is the same Rust that backs the native build, compiled to WebAssembly.

That's the thing I'm most pleased about in dynoxide 0.10.0. It's a big release, and the first one that isn't a clean upgrade. So here's what shipped, why, and what bites you on the way up.

## A database in the browser

Dynoxide started as an engine I wanted to embed in a native app. "Embed it anywhere" was always the pitch. 0.10.0 is the release where the browser counts as anywhere.

The native build backs onto SQLite through `rusqlite`. To run in a browser you need a SQLite that runs in WebAssembly, and that's [wa-sqlite](https://github.com/rhashimoto/wa-sqlite). Dynoxide talks to it over a wasm-bindgen bridge and persists to OPFS. Build it with `--features wasm-sqlite` and you get `WasmDatabase`, the same handlers exposed as `async fn` with no `block_on` in sight.

What works today: create table, put, get, delete, query, and scan, over base tables and both secondary index types (GSI and LSI). Index maintenance is atomic with the base write, same as native. It's enough to back a real client-side app.

What doesn't, yet: TTL returns a typed `Unsupported` error (I haven't implemented it yet). Streams are planned but not wired - the delivery design is the open question. And `TransactWriteItems`, tags, table-setting updates, stats, and bulk import all return a preview "not yet implemented" error. It's a preview, and I'd rather say so plainly than have you find out mid-build.

The detail I like most: no cross-origin isolation. SQLite in the browser usually needs COOP/COEP headers, because the common trick makes an async storage API look synchronous through a `SharedArrayBuffer`. Dynoxide sidesteps that by running wa-sqlite's synchronous OPFS VFS inside a Web Worker, where synchronous file handles are available directly.

So it drops onto ordinary static hosting. No special headers, no cross-origin isolation, no faff.

`npm run build:wasm` gives you a self-contained `dist/` of three files, about 1.2 MB total. The harness under `harness/` loads the exact same bundled worker a consumer would, so a green harness means the shipping artefact works, not a parallel build that happens to pass.

## The trait that made it possible

You can't point one engine at both `rusqlite` and wa-sqlite if the data layer is welded to `rusqlite`. So the real work in 0.10.0 is underneath. There's a `StorageBackend` trait now, and `Database` is generic over it - `Database<S>`. The native rusqlite backend implements the trait, the browser one is `WasmBridgeBackend`, and both go through the same set of SQL builders.

A query fixed on one is fixed on both. That shared SQL is what lets me trust the browser build without a separate conformance run.

The handlers went async, because the browser backend is async by nature. Native keeps its old synchronous API. `NativeDatabase` drives each handler future to completion with `block_on` (via `pollster`), and because the native futures never actually suspend, that `block_on` never parks a thread.

It stays safe inside the tokio-based HTTP and MCP servers, and existing native code that names `Database` keeps compiling untouched.

## The Docker image

Last time I said a Docker image was [coming next](https://martinhicks.dev/articles/dynoxide-patch-notes-0910-0912). Here it is.

```sh
docker run -p 8000:8000 ghcr.io/nubo-db/dynoxide
```

`FROM scratch`, around 5 MB, multi-arch (amd64 and arm64). No shell, no OS, just the static binary. Against DynamoDB Local's ~225 MB Java image, it's a clean drop-in for the containerised test suites people actually run, and [the one someone asked for](https://github.com/nubo-db/dynoxide/issues/3).

It ships a `HEALTHCHECK` backed by a new `dynoxide healthcheck` subcommand, so `docker ps` and Compose health gates report status without you bolting on `curl` or `wget` (there's no shell in the image to run them with anyway). GHCR is the canonical home; Docker Hub and ECR Public get best-effort mirrors on each release.

## What breaks

0.10.0 is the first breaking release, so this part matters.

If you run the binary, there's one change to know about: **MCP over HTTP now requires a bearer token on every request.** A loopback bind generates and persists a token on first run and prints a client-config snippet; a non-loopback bind won't start without one. Existing HTTP-transport MCP clients break until they send an `Authorization: Bearer <token>` header. The stdio transport is unchanged, and plain `dynoxide serve` (DynamoDB only, no MCP) is unchanged.

The reason is the obvious one. An MCP server reachable off-loopback with no auth is an open door to whatever it can touch. There's a `SECURITY.md` now that lays out the threat model, the token, and the Host and Origin allowlists behind it.

If you depend on the Rust crate, there are a few more. `DynoxideError` is now `#[non_exhaustive]`, so an exhaustive `match` needs a `_ =>` arm. `Database` being generic is source-compatible if you just name `Database` (it defaults to the native backend, and there's a `NativeDatabase` alias if you want to be explicit). And `mcp::serve_http` takes an `HttpOptions` struct now instead of a bare port.

## The less glamorous half

Under the two headline features sits a stack of correctness fixes, most of them surfaced by the [conformance suite](https://github.com/paritysuite/dynamodb-conformance) I run alongside dynoxide. A few worth naming:

-   PartiQL `DELETE` and `UPDATE` now evaluate the whole `WHERE` clause, not just the key. The old behaviour could delete a row a filter should have excluded - a genuine data-correctness bug.
-   `DescribeTable` returns a stable `TableId` instead of minting a fresh UUID on every call.
-   `Query` and `Scan` over a GSI stop dropping items when several entries tie on the index key and the base table has only a partition key.
-   `ConsumedCapacity` now matches AWS on the transactional and PartiQL paths.

Where dynoxide actually stands against real DynamoDB and the other emulators lives at [paritysuite.org](https://paritysuite.org). It moves as the suite grows and each engine changes, so I'm not going to pin a number here that's wrong by next week. Go and look at the live one.

## What's next

Streams in the browser, once I've settled how delivery should work without a server to push from. And an npm package for the wasm build, so you can pull it in without wiring up the worker yourself - that path already works, it just isn't packaged.

The browser build is the proof I wanted. The engine isn't tied to one host any more. Native binary, Docker image, or a browser tab: same engine, same SQL, same behaviour.

## Further reading

-    [Dynoxide patch notes: 0.9.10 to 0.9.12
    
    Three patches to dynoxide. 16 error-string fixes, an MCP server that wouldn't die on Ctrl+C, a stale port, and a code comment that lied all along.](https://martinhicks.dev/articles/dynoxide-patch-notes-0910-0912)
-    [Introducing Dynoxide: a fast, embeddable DynamoDB engine
    
    Dynoxide is a DynamoDB-compatible engine in Rust, backed by SQLite, that starts in milliseconds. I built it to embed in a native app, and it turned into something bigger.](https://martinhicks.dev/articles/introducing-dynoxide)
-    [Building a DynamoDB conformance suite
    
    How I built a 526-test DynamoDB conformance suite, and what it found in DynamoDB Local, LocalStack, Dynalite, and Dynoxide.](https://martinhicks.dev/articles/dynoxide-conformance-suite)

---

Source: https://martinhicks.dev/articles/dynoxide-0100-browser-and-docker
