Press Run on the snippet below. The answer comes back in a few milliseconds, and no server of mine was involved.
SELECT status, count(*) AS orders FROM orders GROUP BY status ORDER BY orders DESC;
That query ran inside your browser tab, in a real analytical database compiled to WebAssembly. This post is how that works, why I built the site this way, and the one corner I cut on purpose.
Start with what a server would cost
The usual way to build a SQL practice site is a server that runs your code. You type a query, the browser sends it to an API, the API starts a sandboxed database, runs the query, and sends rows back.
That works, but every part of it has a bill attached. Each run uses CPU on a machine I pay for. Each sandbox has to be isolated, so one learner’s DELETE cannot touch another’s data, and a deliberately slow query cannot starve everyone else. Costs grow with usage: the more people practise, the more it costs. For a site with no paywall, that is the wrong shape. It puts pressure on pricing before there is any reason to charge.
So the design question was: can the work happen somewhere that already has a CPU, and is already isolated per person? It can. That place is the reader’s browser.
The site is just files
First, the boring layer. Built From Bits is a static site. Every lesson, post and problem is an MDX or YAML file in one Git repository. At build time, Astro turns them into plain HTML, CSS and JavaScript. Those files go to a CDN, which copies them to servers close to readers and hands them out.
There is no application server and no database behind the pages. A CDN serving static files is about the cheapest thing on the internet to run. Free tiers cover a site this size. The cost grows with bytes downloaded, not with queries run, and the bytes are cached at the edge.
DuckDB, compiled to WebAssembly
The SQL engine is DuckDB. It is an in-process analytical database: no server, a single library, columnar storage, full SQL with window functions and CTEs. It is the same engine you would use from Python on a laptop.
DuckDB is written in C++. WebAssembly is a compact binary format that browsers can run at close to native speed, and C++ compiles to it. So DuckDB ships as a .wasm file. The build I use is about 36 MB raw. Served with Brotli compression, it is about 5.3 MB over the wire, and it downloads once, then sits in the browser cache.
When you press Run on a page for the first time:
- The page starts a Web Worker, a background thread, and loads DuckDB inside it.
- It fetches the dataset’s
schema.sqland CSV files. All of bitshop is 92 KB. - It creates the tables and loads the rows into memory.
- It runs your query and posts the result rows back to the page.
Steps 1 to 3 happen once per page. After that, each query is just step 4.
Python works the same way. Pyodide is CPython compiled to WebAssembly, and it runs in its own worker. It is a bigger download than DuckDB, so it only loads when you press Run on a Python snippet, never on page load.
Isolation for free
Remember the sandboxing problem. In the browser it disappears. Every reader gets their own copy of the database, in their own tab, on their own machine. A DROP TABLE orders drops your copy. Reload the page and it is back. A query that loops forever burns your CPU, not a shared server, and a timeout kills the worker.
The browser sandbox, which already stops web pages from reading your files, also stops the database from doing anything outside the tab.
How grading works
The practice arena needs to say “pass” or “fail”. For SQL, I did not want to compare your query’s text to mine. There are many correct queries. So grading compares results.
Each SQL problem has a reference query. On Submit, the grader runs your query and the reference query against the same freshly loaded database and compares the two result sets. The rules:
- Order is ignored unless the problem says it matters.
- Column names are compared case-insensitively.
- Numbers match within 1e-6, so
0.1 + 0.2does not fail you. - Duplicates count. The results are compared as multisets: bags of rows where each row has a count.
That last rule is worth a closer look. If the right answer has the row Books, 120.00 twice and your query returns it once, you are wrong, even though every row you returned is correct. Comparing as sets would miss that. Comparing as lists would fail you for a different row order. A multiset is the middle ground: order-free, duplicate-aware.
from collections import Counter
expected = [("Books", 120.0), ("Books", 120.0), ("Stickers", 15.5)]
actual = [("Stickers", 15.5), ("Books", 120.0)]
def as_multiset(rows):
# round floats so tiny differences do not break equality
return Counter(tuple(round(v, 6) if isinstance(v, float) else v for v in r) for r in rows)
print("as sets equal? ", set(expected) == set(actual))
print("as multisets equal? ", as_multiset(expected) == as_multiset(actual))
print("missing rows: ", as_multiset(expected) - as_multiset(actual))The real grader does a bit more, such as aligning columns by name and handling NULLs, but that is the core: count each row, compare the counts.
Hidden tests then change the data. A test can run extra SQL first, like inserting a new product category or cancelling every order in one category. Both queries run against the changed data, the results are compared, and everything is rolled back. A query that hard-codes today’s answer passes the visible test and fails the hidden ones.
The corner I cut
Here is the trade-off. To compare your result with the reference result in your browser, the reference query has to be in your browser. So do the hidden tests. Anyone who opens the developer tools can read them.
I decided that is fine for v1. There are no accounts, no leaderboard and no certificates. Nothing on the site turns a pass into a prize. The only person a peek can fool is the person peeking. And the solution is one click away on every problem anyway, behind a single “try one more hint first?” prompt.
If that changes later, for example a timed interview mode with results that mean something, grading for those problems can move to a server. The rest of the site does not have to change.
What it adds up to
The whole architecture is: static files on a CDN, and two WebAssembly runtimes that do the work on the reader’s machine. Running cost is close to $0 a month at launch and stays close to it as traffic grows. That means price never has to be decided by the hosting bill.
Try it on a real problem. The one below runs entirely in your tab, hidden tests included.
Try it · SQLRevenue by categorySum delivered revenue per product category, biggest first.easy ~10 min →