Real SQLite in the browser, from a Rails app
Running WebAssembly from a strict Rails app touches three things people rarely think about together: your Content Security Policy, your MIME types, and whether the bytes you vendored are the bytes you meant.
The SQL course on this site runs genuine SQLite in the page — not a parser that resembles SQL, but the real engine compiled to WebAssembly. Shipping it from a Rails 8 app with Propshaft, importmaps and a strict Content Security Policy took three changes, each of which is easy to get subtly wrong.
1. Vendor it, and verify what you vendored
The engine is two files: a JavaScript loader and a .wasm binary of roughly 650 KB.
Loading them from a public CDN would have meant widening the CSP to a third-party script
host, and trusting that host on every page view. I host them myself instead.
Self-hosting moves the trust question to the moment you download. So I didn't take the CDN copy on faith: I downloaded the official npm tarball, checked it against the registry's published checksum, and compared both files byte for byte with what I was about to commit. They matched. It took a minute, and it is the only point at which that check is cheap.
A vendored binary is code you ship without reading. The least you owe it is a checksum.
2. The CSP: wasm-unsafe-eval, and nothing broader
With script-src 'self', the loader runs fine and then WebAssembly compilation is
refused. The tempting fix — 'unsafe-eval' — re-enables eval() and new Function()
for every script on the site, which undoes most of what a strict policy is for.
CSP has a narrower keyword for exactly this case: 'wasm-unsafe-eval' permits compiling
WebAssembly and nothing else. Rails exposes it as a symbol:
policy.script_src :self, :wasm_unsafe_eval
Write the reason next to it in the initializer. The word "unsafe" in a security config is the first thing the next reviewer will want to remove, and they should know what breaks if they do.
3. The MIME type nobody checks
It worked. The browser console disagreed: streaming compilation failed because the
binary wasn't served as application/wasm, so it fell back to a slower path. Nothing
was broken, which is precisely how this kind of thing survives to production.
Propshaft serves assets with whatever type Rack knows for the extension. Registering it is two lines in an initializer — one for Rack, one for Rails' own registry — and the warning disappears.
Load it only where it's needed
Six hundred kilobytes is nothing for a page whose whole purpose is a database, and far too much for every other page. The loader script is injected only when a SQL lesson starts, and data files are fingerprinted by Propshaft and looked up through a small manifest in the page head, so caching works and stale files never are.
Test it where it actually runs
The lesson verifier runs the same WebAssembly build under Node, so every SQL lesson's solution executes against real SQLite in CI. The browser test caught the MIME warning; the Node test catches everything else. Neither alone would have been enough.