Back to portfolio Open the live app
Product · SQL education · how it's built

Sprout SQL

A free, public web app where anyone learns SQL by writing real queries against a real database — and gets instantly, provably correct feedback. The defining promise is correctness: a learner is never told a wrong answer is right, or a right answer wrong.

7
worlds
118
lessons
181
graded exercises
100%
free
Sprout SQL landing page: Write real SQL, watch it grow, with a live query editor and 7 worlds, 118 lessons, 181 exercises
The live landing page — a real query editor above the fold, no signup to try.

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.

The non-negotiable constraintAnswers are graded by running a verified reference query through the same engine as the learner's query and comparing result sets — never by comparing text. Every reference query ships only after passing automated engine verification. This is the product's reason to exist, and it overrides convenience, speed, and visual polish.

What builds what

LayerTechnology
UIReact + TypeScript
Build toolVite (with correct WASM asset handling)
SQL enginesql.js — SQLite compiled to WebAssembly, in the browser
Auth + dataSupabase — Auth, Postgres, Row-Level Security
HostingVercel — 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.

Why this belongs on an analyst's portfolioBeyond the build, Sprout SQL is specified like delivery work — requirements with IDs, acceptance criteria, a verification plan that gates every release. Writing that clearly, and holding a correctness rule over convenience, is the same discipline a business analyst brings to a real project.