The developer training environment. Hand-checked lessons on a staged path - the ten-stage developer path plus C and C++ tracks - each stage capped by a mandatory test, all graded for real. Two ways through: Learn mode teaches each new idea before asking about it (theory → example → try it → quick check → practice); Practice mode goes straight to the questions. Wrapped in a landing page, a dashboard with XP, streaks, ten roadmap.sh-style roadmaps, a reading article per stage, a leaderboard and an admin console.
834 lessons and 12 stage tests across 12 stages on 3 tracks (10 free, 2 premium) - Developer path: 724 lessons in 10 stages · C: 55 lessons in 1 stage · C++: 55 lessons in 1 stage.
Everything runs on your machine. No cloud account, no Docker, no native build step.
Live demo of the front end: https://devlingo-sand.vercel.app/ (static hosting only, so it runs in guest mode - see Deploying below).
npm install
npm run devThen open http://localhost:3000. That starts two things:
| web | Vite dev server on :3000 |
| api | Node API on :4000, proxied at /api |
The app works with the API down — progress falls back to localStorage and
JavaScript runs in a Web Worker instead — it just tells you so rather than
failing silently. If the API is simply slower to boot than Vite, the page
retries and connects on its own.
To run it as one process instead:
npm run build
npm start # app + API together on http://localhost:4000| Route | What is there |
|---|---|
/ |
Landing page - hero, how it works, a real challenge to try, your own streak and level |
/dashboard |
Streak, XP, level, "continue learning", activity heatmap, daily goals, achievements |
/dashboard/learn |
Pick a track (developer path, C, C++), choose Learn or Practice, and work the stages and their tests |
/dashboard/challenges |
Search and filter every challenge, by track, stage, type, difficulty and status |
/dashboard/practice |
Playground - run JavaScript or Python for real; Java, C and C++ through Judge0 when configured |
/dashboard/roadmap |
Developer roadmaps (roadmap.sh-style) plus your track as one connected path |
/dashboard/roadmap/:slug |
One roadmap: topic graph, per-topic notes and links, done/learning/skip tracking |
/dashboard/learn/:stage/read |
The stage's article - the reading that goes with its lessons |
/dashboard/leaderboard |
This week's league and the all-time board, ranked by server-verified XP |
/dashboard/achievements |
Figures, coverage by language, badges |
/dashboard/settings |
Account, Pro unlock, theme, learning mode, track, reset |
/admin |
Administrator console - its own sign-in, users, tracks, stages, challenges, analytics, audit log |
You can use every page as a guest - progress is kept in the browser and merged into an account when you sign in. Light and dark themes throughout.
The lessons (counted at the top of this file; Stage 1 opens with two extra concept-led lessons) come in seven formats, plus one stage test per stage:
| Type | What you do |
|---|---|
quiz |
Pick the right answer, usually about a snippet |
output_prediction |
Say exactly what a program prints |
multi_select |
Pick every answer that applies |
fill_blank |
Type or choose the missing tokens, inline in the code |
pseudocode_order |
Drag scrambled pseudocode into the one correct order |
code_runner |
Write a function; it is graded by real test runs |
debug |
Find and fix a planted bug |
The developer path runs Programming Basics → Python → Data Structures → Algorithms → Web → Backend → SQL → Git and Testing → System Design → Shipping. C Fundamentals and C++ Fundamentals are separate tracks with their own lock chain - switch tracks from the Learn page or the sidebar; each keeps its own progress, all of it in the one account.
Learn mode and Practice mode. The first stage you open asks how you want to learn, and remembers the answer (change it any time from the modal header, the Learn page or Settings). In Learn mode a lesson that introduces a new idea first walks through it - a plain explanation, an annotated example, why it works, a tiny ungraded "try it" that really runs, then the quick check - and a wrong answer explains itself immediately, naming the option you picked. The lessons that follow are marked as practice of that idea. Practice mode shows the question straight away and keeps the "have another look" rule. Both modes use the same challenge engine, grading, XP, streaks and stage unlocking; only the journey to the question differs.
Stage tests. Every stage ends in a mandatory test on its own topic - a coding problem wherever the language has an engine here (run-length encoding for Basics, an LRU cache for Data Structures, a sliding window for Algorithms, a route matcher for Backend, an in-memory INNER JOIN for SQL, a rate limiter for System Design, and so on), and an answer-graded problem for C and C++, which have no local compiler. It unlocks once every lesson in the stage is solved, and the next stage in the track stays locked until it is passed. Tests are presented LeetCode-style: statement, worked examples, constraints, a mix of visible and hidden test cases, and no hints until you have made an attempt. Nothing can be skipped.
Reading before doing. Every stage has an article (src/modules/articles/content/*.md,
about 10 minutes each) split into sections tagged with the concepts they explain.
The stage card offers it as "Read first", every challenge card links to the exact
section, and inside the practice modal a "Read about this topic" panel unfolds the
matching section above the prompt - reading is never penalised. npm run content:extras checks that every core-path challenge maps to a section by tag.
The C and C++ stages have no article yet (their lessons carry the concept
walk-throughs instead); the check reports it and their cards offer no "Read first".
Roadmaps. Ten roadmap.sh-style maps - Frontend, Backend, Full Stack, DevOps,
JavaScript, Python, SQL, Computer Science, System Design, Git & GitHub - with 200+
topics. Each topic has a short explanation, curated free resources (official docs,
MDN, free courses and books), a link to the community version on roadmap.sh, and,
where CodeConsist covers it, a jump into the stage's lessons and article. Topics can
be marked done / learning / skipped; that lives in the browser and earns no XP,
because nothing verifies it. The roadmaps' structure and text are CodeConsist's own:
roadmap.sh's content is copyrighted and is linked to, never copied.
npm run content:links HEAD-requests every external URL.
Four real execution engines, and no faking.
- JavaScript runs in a sandboxed Node child process (
node:vm, norequire, nofs, hard-killed after 8s). If the API is down it runs in a Web Worker, which the main thread can terminate — the only way to stop an infinite loop. - Python is CPython compiled to WebAssembly (Pyodide) in a Web Worker, so an infinite loop is killed after 8s instead of freezing the tab. Each run gets a fresh namespace; the runtime itself is downloaded once per session.
- HTML, CSS and JavaScript render together in the Playground's web mode: three
files in an
<iframe sandbox="allow-scripts">with no same-origin access, so the page cannot reach the app, its storage or the network.console.logis relayed out to a console beside the preview, and a script that never returns wedges that one frame — Run builds a new one. - C, C++, Java and Go run only if you point the API server at a Judge0
sandbox. Nothing submitted is ever compiled or run on the host:
/api/executeneeds no sign-in and is reachable through the public tunnel, so there is nogccand nojavacbehind it. The free way is Judge0 in Docker on the same machine —JUDGE0_API_URLalone, no key, no rate limit, nothing leaving the laptop (docs/RUNNING-JAVA-C-CPP.md); a hosted RapidAPI endpoint takesJUDGE0_API_KEYas well, and that key never reaches the browser./api/healthreports only which languages run and what runs them, so the Playground can label the rest "needs setup" rather than pretending to compile.
Accounts and progress. bcrypt password hashing, JWT sessions, XP, levels,
day-based streaks and a leaderboard, stored in server/data/db.json (atomic
writes, so a crash mid-save cannot truncate it). Play as a guest and your local
progress is merged into the account when you sign in.
The server owns the verdict as well as the XP. A solve sends what you answered; the server re-checks it - quiz, blanks and ordering by the same rules the browser used, JavaScript by running the tests in its own sandbox, Python through a local CPython when one is installed - and only then pays out. Solves made while the API was unreachable are kept locally and pushed up on the next visit.
Sandboxing. Submitted JavaScript runs in a node:vm context that receives
nothing from the host realm - not even a console callback, since any host
object's .constructor chain leads back to the real process. Code
generation (eval, new Function) is disabled inside it. Do not loosen this.
Administrator console. /admin is a separate app with a separate credential
system - an Admin User ID and password you choose in .env (ADMIN_USER_ID,
ADMIN_PASSWORD) the first time the server starts; the password is hashed with
bcrypt, stored once, and never read from .env again. It is not a learner
account, never appears in the leaderboard, and its 12-hour token is invalidated
the moment the credentials change. From there you can see users and their
progress, hide or reorder stages within a track, re-word or re-price a
challenge, unpublish a track, read the audit log and, optionally, mirror
learner accounts into an Excel table through Microsoft Graph. Every one of
those edits is an override layered on the authored content in db.json -
the questions themselves stay in TypeScript where npm run check executes
them. Every /api/admin/* route re-checks the admin token against the live
admin record on each request; nothing in the browser is the gate.
| Command | Does |
|---|---|
npm run dev |
API + web together |
npm run dev:api |
API only (restarts when content changes) |
npm run dev:web |
Web only |
npm run build |
Type-check and build to dist/ |
npm run preview |
Serve dist/ as production would (guest mode; no API) |
npm start |
Serve the built app and the API from one process on :4000 |
npm run check |
Everything below, in sequence - required before a merge |
npm run lint:boundaries |
Enforce the dependency direction between areas and modules |
npm run content:parity |
Prove the Vite and Node content loaders select the same files |
npm run test |
Unit and integration tests (vitest) |
npm run content:validate |
Correctness check: structure (incl. concepts and tracks) + really run every solution |
npm run content:lint |
Quality check: leaked hints, duplicate options, answer bias |
npm run content:extras |
Validate the articles and roadmaps (sections, tags, node ids, URLs) |
npm run content:stats |
Check that the content counts in this README and the package.json description match the bank; node scripts/content-stats.mjs --write updates them |
npm run content:links |
The same as content:extras, plus a live check of every external link (not part of check: it needs the network) |
Challenges live in src/modules/challenges/content/<topic>/<batch>.ts, each
exporting challenges: Challenge[]. Stage tests live in
content/stage-tests.ts with isStageTest: true. Stage metadata is
content/stages.ts and the tracks (which stages form which path) are
content/tracks.ts. Every other file under content/ is discovered
automatically - a new topic is a new folder, with no index to regenerate.
docs/CONTENT_AUTHORING.md has the rules, including how to give a lesson a
beginner concept; content/programming-basics/a.ts shows every type and the
first two lessons show concepts.
After adding a file:
npm run checkContent goes through the content registry (src/platform/content-registry):
a zod schema per content kind (modules/<name>/schema.ts) is applied at build
time by the same code the browser, the API server and the scripts all use, so
a malformed challenge fails with its file path and field rather than crashing a
learner's session. scripts/validate-content.mjs then adds what a schema
cannot express, and for every executable challenge it runs the reference
solution against every test case — and separately checks that a debug
challenge's starter code actually fails. A challenge that cannot be solved as
written will not pass.
JavaScript runs in a Node VM. Python runs through a local CPython 3 if one is on
PATH (python3, python or py); if there is none the run says so explicitly
rather than quietly reporting those challenges as verified.
npm run build && npm start serves the app and the API from one Node process,
which is the whole thing. A static host (Vercel, Netlify, GitHub Pages) can only
serve dist/: the app then runs in guest mode - progress in
localStorage, JavaScript graded in a Web Worker, Python via Pyodide - and
says so, but there are no accounts, no leaderboard and no server-verified XP.
For the full experience host the API on a Node service and point the front end
at it with VITE_API_PROXY (dev) or same-origin npm start (prod).
vercel.json adds the SPA rewrites client-side routing needs.
A modular monolith with one dependency direction, enforced by
npm run lint:boundaries (see docs/adr/):
app → modules → platform → ui / types / config
src/
app/ composition root: providers, lazy routes (routes/), the shell,
the async content loader, content-specs.ts
modules/ business domains - same shape in every folder, own README
challenges/ the bank (content/<topic>/, stages.ts, tracks.ts), 7 challenge
types (a registry), the concept walk-through (components/lesson/),
Learn/Practice chooser, practice modal + session, learning path, library
admin/ the administrator console at /admin (own session, own chunk)
articles/ per-stage reading (content/*.md), reading panel, article page
roadmaps/ roadmap.sh-style maps (content/*.ts), topic progress, hub/detail
dashboard/ leaderboard/ achievements/ playground/ account/ landing/
platform/ infrastructure any module may use
session/ identity, verified progress, content bank, execution, leaderboard
events/ the typed event bus modules talk through
content-registry/ discovery + zod validation shared by Vite and Node loaders
grading-engine/ grading primitives (shared with the server), answer helpers
xp-leveling/ level curve, streaks, scoring, derived insights
progress/ stage unlocking, evaluated once per language track
execution/ JS sandbox worker, Pyodide worker, Judge0 client
api-client/ storage/ theme/ markdown/
ui/ presentational primitives (CodeBlock, CodeEditor, toasts, hooks, theme CSS)
types/ contracts shared across areas
config/ routes, env
server/ Express API — auth, progress, grading, execution, admin
env.js loads .env first (in code - see docs/adr/0007)
content.js loads the bank through the content registry, caches to JSON,
merges admin overrides
auth.js / admin-auth.js / admin.js learner and administrator sessions, the admin API
excel.js optional, best-effort Microsoft Excel mirror of learner accounts
runner/ the sandboxed child process that runs submissions
scripts/ validators, linters, parity and boundary checks
docs/adr/ why the structure is the way it is
Modules never import each other. They cooperate through platform/events
(challenge:completed, practice:open, …) or through props the app passes in
(readingFor, relatedFor).
Every screen is its own chunk and the challenge bank is another, fetched with
import() after the shell paints (docs/adr/0006). Production build:
| Chunk | Loaded | gzipped |
|---|---|---|
index (providers, router, layout, practice modal) + react + icons |
first paint | ~186 kB |
challenges-content |
right after, in parallel | ~109 kB |
landing |
on / |
~8 kB |
dashboard, route-learn, roadmaps, articles, … |
the screen you open | 2-43 kB each |
admin |
the admin console | ~98 kB |
types (zod + the schemas) |
the admin console only (settings validation); development and CI for content | ~12 kB |
The learning loop (settings, streaks and goals, the activity log, Practice
sessions, test-out and placement) grew the shell by about half, from ~126 kB
before it. The admin's settings metadata (src/platform/settings/meta.ts)
and the test-out and placement screens (AssessmentPanel) stay out of it.
node scripts/check-dist.mjs, run by CI after the build, fails when the shell
passes its gzipped budget (195 kB, set in that script) or picks up zod,
framer-motion or that metadata. framer-motion is still in package.json, and
vite.config.ts still names a motion chunk for it, but nothing imports it
today.
Content-only releases leave every UI chunk cached in returning browsers, and
the other way round. npm run build prints the table; the boundary check
refuses a static import of a content bank, which is what would silently put it
back on the critical path. src/platform/grading-engine/grading.ts and
src/platform/xp-leveling/leveling.ts are compiled and imported by the server
too, so "correct" and "level 4" mean the same thing on both sides.
None required. Copy .env.example to .env for the optional knobs (API port,
JWT secret, Judge0 credentials, the administrator bootstrap, the Excel mirror).
The API loads it itself (server/env.js) and Vite reads it for the dev proxy.
Styling is Tailwind CSS v4 for pages and layout, with a small amount of plain
CSS (src/styles) for the practice modal and editor, both reading the same
theme tokens. Dark mode is the .dark class on <html>.
The supabase/ directory is left over from an earlier hosted design and is not
wired up — the local API replaced it.
- Aditya Raj (@Aditya-raj11)
- @HYERx-design