Student app
- Teacher-assigned homework and Q&A
- Installable PWA, playable demo before sign-up
A gamified learning platform for a foreign-language school: a content-driven game engine, four role-separated consoles, an AI content pipeline and a progress economy that runs entirely inside Postgres. 238 components, 34 tables, 42 database functions. Built and shipped by two engineers.

The school had to be able to add content without us.
A learning product is only alive while new material keeps arriving. If every new mission, word list or illustration needs a developer and a deploy, the platform stops growing the week the budget ends.
Everything a lesson is made of had to become data.
Districts, locations, missions, tasks, vocabulary and rewards live in Postgres and are authored in an admin console. The code holds the mechanics; the database holds the curriculum. Five engineering problems followed, and they are the interesting part of this project.
Four applications on one schema, 42 routes, 16 feature modules. Every item below is shipped and in production.
Crossword, hangman, word search, spot-the-difference, snake, flashcards, sentence builder, clock reading, analogies: each type is a React component paired with a typed parser that turns a JSON row into a strict model. TaskRunner reads the task type off the row and mounts the right pair.
A malformed row fails in its parser with a typed error instead of crashing a mission halfway through. Puzzle generation (word-search grids, crossword layout, snake boards) sits in pure functions with their own unit tests, separate from rendering.
Adding a 33rd type is one component and one parser. Nothing else in the engine moves.
XP, coins, streaks and opened locations are computed by SECURITY DEFINER Postgres functions, reached only through Next.js Server Actions. Row level security is on every user table, and the browser is never granted a direct write to progress, stats or the wardrobe.
Purchases, equipping gear, homework completion and study time all take the same route, so there is one place where each rule exists. A stolen anon key still cannot award a single coin.
Streak rolling runs in the one Deno Edge Function, with its date logic split into a dependency-free module so it can be unit tested outside the runtime.
One Server Action per feature, one Postgres function per mutation.
Role checks live in RLS policies, not UI guards. A request that should not be allowed fails at the database, whatever the client believes it is permitted to do. There is no service-role key anywhere in the project, so nothing can bypass them.
Reads and writes nothing outside their own profile and progress. A level opens only once it has published content.
Promotion-only: an admin flips an existing account through a dedicated function. Owns groups, topics and their threads.
Sees exactly the students linked to their account, and nothing about anyone else’s child.
Self-claimed from an allowlist, never assignable from the client. Authors content and manages every account.
Location art, task illustrations, flashcard images and homework drafts are generated by server actions through OpenRouter: one model for images, another for structured JSON content. The key never reaches a browser.
Image requests are pinned to a flex queue: the same model at half the price on a best-effort lane, with provider fallbacks left on, so an unavailable queue degrades to the standard tier instead of failing the editor who is waiting. Every generation is logged with the tier that actually served it and what it cost.
Results land in shared cache tables keyed by prompt, so repeated vocabulary and task art costs nothing the second time.
Every merge runs lint, type-check, unit tests and a production build, then applies pending migrations to the database on its own. 66 migrations so far, each one reviewed in a pull request, each one live the moment it merges.
Database types are generated from the schema and committed with the migration that changed it, so a column rename becomes a type error rather than a runtime surprise. Every game-state mutation has tests asserting the server-side validation actually runs.
Shared UI primitives are documented in Storybook with an accessibility addon, and component tests run in a real browser through Playwright.
Native apps on React Native, over the same backend.
The engine, the content model and the server-authoritative rules do not move. The client does. Everything the database decides today it keeps deciding for the mobile apps.
Fixed price · Fixed Scope · Senior only