The problem it solves
Most “learn SQL” tools fall into one of two traps. Fake graders pattern-match your text against an expected string, so they reject correct answers written a different way and accept wrong ones that happen to look right. Read-only tutorials never let you actually run a query. Sprout SQL solves both by running a genuine SQL engine in the browser and grading by comparing result sets, not text.
What builds what
| Layer | Technology |
|---|---|
| UI | React + TypeScript |
| Build tool | Vite (with correct WASM asset handling) |
| SQL engine | sql.js — SQLite compiled to WebAssembly, in the browser |
| Auth + data | Supabase — Auth, Postgres, Row-Level Security |
| Hosting | Vercel — static site over a global CDN |
There is no custom server: Supabase is the backend. The heavy work — executing SQL — happens in each browser, so ten thousand concurrent learners are ten thousand browsers doing their own compute, while the database sees only a trickle of tiny progress writes. That is why it stays cheap and scales by configuration, not rewrite.
The one technical decision that matters most
The WebAssembly engine is loaded through Vite's ?url import, not by hand-copying a file into a public folder. sql.js ships more than one engine binary and a bundled browser build asks for a specific filename; guessing that URL is fragile, and a mismatch is exactly what broke an earlier attempt — the app requested one filename while another had been copied, so the server returned an HTML page and the engine refused to load. The ?url import hands resolution to Vite, which always points at the real file in dev and production, removing the entire class of “engine could not load” failures.
How grading actually works
Each lesson runs against a fresh in-memory database built from a schema and deterministic seed, so prior runs can't affect the result. The learner's SQL runs; the lesson's verified reference query runs on the same fresh database; and a compare() function checks columns, then rows, then values — ordered when the lesson says order matters, otherwise as an unordered multiset, with numbers normalised so 4 and 4.0 match and NULLs compared explicitly. Column names are never required to match. On success it awards XP and advances; on failure it names the specific difference — wrong column count, wrong row count, wrong order, or wrong values.
The worlds
Seven themed databases of escalating difficulty, each authored from scratch, seeded deterministically, and every reference query engine-verified — from a corner shop through streaming, a football league, e-commerce, finance and healthcare, up to an interview-prep world. A learner who counts the data will find no contradiction: records are reconciled so, for example, a league's individual goals exactly match its scorelines.
Security model
Row-Level Security on the progress table is the core protection: every browser uses the same public anon key, but Postgres policies let a user read and write only the row whose id matches their authenticated id. The privileged service key is never in client code or the repo, auth is delegated entirely to Supabase, and no user-supplied SQL ever runs on a server — it executes only in the browser sandbox, so there is no server-side injection surface.
Build → Verify → Package
The governing discipline: no content ships until it has been verified by the real engine and the load path confirmed, and the answer key is engine-produced, never hand-typed. A verification script loads the engine, builds each dataset, runs every reference query, self-grades it, and runs negative probes to prove the grader rejects wrong answers rather than just agreeing with itself. The result must be N-of-N pass, zero fail — and adding a lesson is adding one content object plus re-running verification, with no UI rewiring. The whole thing is specified across a six-document set: a PRD, a software-requirements spec, technical architecture, the data and content model, a UI/UX design system, and this verification and deployment plan.