Ruby & Rails

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.

7 October 2026 3 min read Mohammad Aaquib Jawed

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.

All writing Reply by email