
Updated
Replacing a native SQLite dependency
A Node.js ABI mismatch led us to a WebAssembly SQLite build. The dependency became easier to install, but the adapter still needed careful testing.
The engine installed successfully, then failed when it opened its database:
The module was compiled against a different Node.js version.
This version of Node.js requires a different NODE_MODULE_VERSION.
The native SQLite module didn’t match the running Node.js ABI. The failure happened before the user could do any useful work.
This is an engineering note about the migration we made in April 2026. It explains the tradeoff behind that change, rather than promising support for every runtime a WebAssembly engine can run in.
The first fixes kept the same dependency
We first made initialization failures visible. A swallowed exception had turned a database problem into an unexplained failure later in the program. Throwing an actionable error was a worthwhile improvement on its own.
We also added installation logic to resolve the native binary for the current runtime. That addressed the immediate mismatch, but left us maintaining another installation path. The engine used SQLite for scan history and findings; it didn’t need a native database binding for a demanding throughput workload.
We replaced better-sqlite3 with node-sqlite3-wasm.
The query layer still expected the old driver
Changing the package name wasn’t enough. Our Drizzle integration expected synchronous prepared statements with methods such as run(), get(), and all(). The replacement exposed a different interface.
We wrote an adapter for the subset the engine used. It translated statement calls and bound parameters, and connected the replacement database to the existing query layer. We also avoided an import path that loaded the native driver as a side effect.
That last detail mattered: an adapter doesn’t remove a native dependency if importing the adapter still loads it.
The compatibility work needed to cover more than successful queries. Empty results, parameter binding, transaction rollback, migrations, and errors all belong to the database contract. A wrapper that gets ordinary inserts right can still fail on one of those boundaries.
What changed operationally
The WebAssembly build removed this database component’s dependence on a Node native-addon ABI. It also let us remove the workaround that selected a replacement native binary during installation.
There was a tradeoff. The replacement’s filesystem implementation didn’t support the WAL configuration we had used, so we removed that setting. That was acceptable for our workload at the time. It wouldn’t establish suitability for a service with concurrent writers or different durability requirements.
We also needed fresh install-and-run checks on supported runtimes. WebAssembly portability doesn’t guarantee that a package’s filesystem access, module loading, or surrounding JavaScript will work everywhere.
A smaller installation problem
The migration moved complexity from platform-specific installation into a small adapter we could test directly. For this engine, that was a useful exchange.
It didn’t eliminate every startup failure or prove that future Node releases would work unchanged. It removed a particular failure mode that had reached users before they could run their first scan.