Overview
What it isCodingo is a free, gamified way to learn programming on the web — modelled on Duolingo’s bite-sized daily habit but for code. A single JavaScript course ships today (30 lessons across 5 units); every exercise runs in your browser so there is no server cost per run.
Instant feedback
Check answers inline, get XP, see the confetti. Scores below 80% can be retried immediately.
Daily habit inside
Streaks, daily XP goals, hearts that regenerate every 4 hours, and badges that mean something.
Never stuck
Ask per-lesson doubt threads — peers reply and a hint-first AI answers within three minutes when free.
How it works — the 60 second tour
Create an account → finish onboarding
Email/password or Google. Onboarding asks your name, age, country and language (only JavaScript is live) — then you land on /app.
Open the path → run a lesson
/app/learn shows a vertical skill path. Tap a node, solve 5–8 exercises one screen at a time — your answers, draft and resume point are saved after every tap (even on airplane mode).
Earn and return
First completion mints XP + Codingo Cash and advances your level, streak and badges. Miss a day? A freeze can save you. Leaderboards live at /app/leaderboard, community at /app/community.
Learning path
One course today: JS from Zero — from zero to advanced, no prior code needed. 5 units, 30 lessons, gentle→code-heavy.
| Unit | Lessons |
|---|---|
| Unit 1 — Fundamentals | What is Code? · Variables · Types · Operators · Template Literals · Checkpoint |
| Unit 2 — Control Flow | If / Else · Comparisons & Logic · Switch & Ternary · For Loops · While Loops · Checkpoint |
| Unit 3 — Functions & Scope | Functions Basics · Parameters · Scope · Arrow Functions · Callbacks Intro · Checkpoint |
| Unit 4 — Data Structures | Arrays Basics · Array Methods · Objects Basics · Strings & Arrays · Errors & Debugging · Checkpoint |
| Unit 5 — Async & Project | DOM Basics · Events · Timers & Callbacks · Promises · Async Await & Fetch · Mini Project: Todo App |
Seed: bun src/seed/seed.ts wipes then inserts exactly this curriculum. Order matters — nodes increment strictly by order.
Seven exercise types
Concept
Multiple choice · Fill in the blank
One correct option or one blank string. Validated by strict equality against solution.correctIndex/answer.
Recall
Predict output · Arrange blocks
Trim-normalized output match; block order is JSON.stringify of the index array.
Coding
Fix the bug · Write code
Monaco editor + Web Worker runCode against the first tests[0].expected (trim + CRLF normalize).
Meta
AI prompt
Learner writes a prompt, checks a checklist and marks “asked/example”. Only then is the exercise considered ready.
Gamification
XP & levels
First completion only: 5 × correct + 10 lesson + 5 perfect, capped total×5+15. Levels 100 → 250 → 450 → 700 then +150 per level.
Thresholds: lib/level.js and backend/src/utils/level.ts must stay in sync.
Streak & daily goal
Streak counts in Asia/Kolkata by default. Daily goal is 50 XP (10–200, snaps to 20/30/50/80/100). Fail to hit a day and a freeze is spent if you own one; otherwise the streak resets.
Codingo Cash (CC)
Start with 50. Earn 5 per completed lesson + 10 if perfect. Spend on hearts and freezes — the only purchases.
Hearts
Cap 3, regenerates 1 per 4 hours, single 20 CC, full 50 CC. Checked passively on every auth and progress call.
Badges
Unlocked only on first lesson-completion and deduped by a Set. Source of truth lives in utils/badges.ts.
Community & AI
Doubt threads
- One thread per doubt, optionally linked to a lesson —
GET /api/threads?lessonId&sort=new|top&limit&before. - Live updates via SSE
GET /api/threads/stream?lessonId(heartbeat :/25s). - Replies sorted accepted → votes → time; upvotes are toggle + clamp, accepts are asker-only.
- Reports are deduped (409) and never reveal reporter identity.
AI doubt helper
- Groq primary, OpenRouter fallback — OpenAI-compatible chat completions, 30s timeout, temp 0.7.
- Hint-first, ≤130 words + ≤one 6-line snippet, Hinglish-mirroring, never leaks
solution. Prompt sources:lessonTitle + exercise prompt + learner codeonly. - 20 per day per user (
AI_DAILY_LIMIT), counted inAiUsage. Cache hits inAiCache (sha256)cost nothing. - New threads get an auto first reply after ~3 min (
AI_AUTO_REPLY_DELAY_MS) only if no peer answered and AI is configured — authored by the system usercodingo-ai.
Architecture
Single repo, two deployables, one database. The browser never talks directly to the backend domain — it talks same-origin to Next.js and Next.js rewrites /api/* to BACKEND_URL so the codingo_token cookie stays first-party.
browser → Next.js (vercel: codingo.synax.me)
│ rewrite /api/* → BACKEND_URL (render)
└──────────────→ Express (Bun) → MongoDB Atlas
│ next/font, Tailwind v4, Zustand, Monaco
│ IndexedDB progressDb + SW (offline-first)
└ helmet · cors(allowlist) · trust proxy 1 · requireDbMonorepo
frontend/ and backend/ each have their own package.json and env. No shared build — keep it boring.
Auth boundary
Edge proxy.js does a cookie-existence redirect; the real JWT verify is in server components via GET /api/auth/me.
Data ownership
MongoDB is the source of truth; IndexedDB is a per-user cache + offline queue that replays when back online.
Frontend
Next.js 16.3.5 App Router, React 19, Tailwind v4 — server components by default, Client islands only where interactive. Public pages (/, /docs, /u/[username], legal) need no login; /app/* and /onboarding are guarded.
| Route | File | Auth |
|---|---|---|
| / | app/page.js | public → redirects if authed |
| /login · /signup · /forgot-password | (auth)/*/page.js | guest (AuthLayout) |
| /onboarding | onboarding/page.js | authed + !completed |
| /app | app/app/page.js | dashboard |
| /app/learn · /app/learn/[lessonId] | app/app/learn/** | path + runner |
| /app/community · /app/community/[threadId] | app/app/community/** | feed + detail (SSE) |
| /app/leaderboard | app/app/leaderboard/page.js | global board |
| /app/profile · /app/settings | app/app/** | self |
| /u/[username] | app/u/[username]/page.js | public showcase |
| /docs · /privacy · /terms · /policy · /offline | app/docs|privacy|…/page.js | public |
Key libs
lib/api.js— single API helper (handles FormData, timeout 30s, cookie forwarding)lib/progressDb.js v3— 4-store IndexedDB (progress/meta/pending/drafts)stores/progressStore.js— Zustand with IDB-first hydration + offline queuelib/runner— Worker JS sandbox +compareOutput
PWA + perf
public/sw.js(must-revalidate) +RegisterSW+InstallAppsite.webmanifest+sitemap.js+robots.js+ OG JSON-LD on/- Images
correct/wrong/complete.mp3and confetti on lesson complete
Backend — Express on Bun
Entry src/index.ts connects Mongoose, builds the Express app via createApp() (trust proxy 1, helmet, parsers, strict CORS in prod, requireDb fail-fast), mounts 8 routers.
Auth — /api/auth
- POST register · POST login · POST logout · GET me · PATCH onboarding · POST google
Google verifies ID token audience + email_verified via google-auth-library; temp google_user_* is replaced during onboarding. Rate limit 20/15m.
Content — /api/courses · /api/lessons/:id
Courses return nested units→lessons sorted by order. Lesson detail needs auth and returns ordered exercises.
Progress — /api/progress
POST upserts {lessonId, score 0..100, completed?, firstTry?, correctCount, total}; awards XP/CC/levels/badges/streak only on first completion. GET me and GET :lessonId for reads.
Community — /api/threads + SSE
Cursor feed (?lessonId&sort&limit&before), SSE /stream?lessonId with : heartbeat every 25s, upvote toggle+clamp, asker-only accept, deduped reports. In-process pub/sub via utils/communityEvents.
Users — /api/users · Leaderboard · Economy · AI
PATCH /users/me+POST /users/me/avatar(Appwrite via multer 2MB, 501 until configured) +GET /users/u/:username(email never leaked, private hides stats).GET /leaderboard?limit&offset&page(onboardingCompleted, public, sort xp desc + createdAt asc) with viewer rank even when off-page.GET /economy/me·PATCH /economy/daily-goal·POST /economy/hearts/consume|refill·POST /economy/freeze/buy.GET /ai/status·POST /ai/help(20/m limit, budget 20/day, cached, never leakssolution).
Every non-health route goes through requireDb — if Mongo is down the backend returns 503 Database not connected instead of hanging.
Data models
All Mongoose. username/email unique with collation en strength 2 for case-insensitive dedupe; googleId sparse unique; progress unique on {userId,lessonId}.
User
username 3–30 / email / password select:false / googleId / name / avatar / avatarFileId / bio≤160 / isPrivate · xp / level / streak{count,lastActiveDate} / timezone Asia/Kolkata · badges[] / age 13–80 / country / countryCode / language / onboardingCompleted · cc 50 / hearts 3 / heartsUpdatedAt / dailyGoalXp 50 / dailyXp / dailyXpDate YYYY-MM-DD / freezes + timestamps. Extra indexes on {xp:-1,createdAt:1} for leaderboard.
Course → Unit → Lesson
course.order / unit{courseId,order} / lesson{unitId,order,xpReward 10}
Exercise
{lessonId, type 7-enum, prompt, content Mixed, solution Mixed, explanation, hints[]}
Progress · XPEvent
{userId,lessonId, status, score, bestScore, attempts, firstTry, completedAt} + {userId,lessonId?,source,amount}
Thread · Reply · Report
{lessonId?,authorId,title,body, votes, upvotedBy[], replyCount, acceptedReplyId} · {threadId,authorId,body,isAi,isAccepted} · {targetType, targetId, reporterId, reason}
AiUsage · AiCache
{userId, day YYYY-MM-DD, count} per user/day. {key sha256, answer, provider, modelName} global.
Auth guard
Validators in src/validators/auth.ts (Zod) plus inline Zod per route; JWT in codingo_token httpOnly cookie.
Course 1──M Unit 1──M Lesson 1──M Exercise
User 1──M Progress M──1 Lesson
User 1──M XPEvent
User 1──M Thread 1──M Reply
AiUsage (per user/day) · AiCache (global, sha256)Offline & PWA
Progress is never lost when a tab is closed mid-lesson. Every layer is built to survive a dropped connection.
IndexedDB v3 — four stores
progresskey userId:lessonId— authoritative docsmeta userId— cached{xp,level,streak,badges,lastSync}pending …:ts:rand— offline queue drained when back onlinelesson_drafts userId:lessonId— runner resume (idx + answers + checked + firstTryCorrect)
Module: lib/progressDb.js · store: stores/progressStore.js (hydrate → fetch → merge pending → syncPending).
Save flow
- Optimistic write to memory +
putProgressinstantly. POST /api/progress— on success replace with server doc; on fail push topendingand keep optimistic.syncPendingon reconnect drains pending; new fetches never clobber pending completions.- Drafts persist on every 300ms tick plus
beforeunload / visibilitychange / pagehide; back button shows Exit confirm modal.
Try it: start a lesson, close the tab on exercise 3, reopen — you resume exactly at 3. Go airplane-mode, complete it, see “Saved offline — will sync”, then reconnect.
Code runner
JavaScript runs sandboxed in workers/jsRunner.worker.js via lib/runner/runJS — timeout 2s, output cap 10KB. Python is stubbed for Pyodide lazy-load. Comparison is trim + CRLF normalize.
// lib/runner — runCode picks the adapter
await runCode({ code, language: "javascript", timeout: 2000 })
// → { output, error, timedOut, truncated }
compareOutput(actual, expected) // trim + \r\n→\n
// Python today:
// → { notImplemented: true, error: "Python runner not yet loaded..." }Pluggable by design — adding a new language is a new branch in runCode without touching any route.
Design system
Single source DESIGN.md → app/globals.css @theme. One saturated green owns “correct/progress”, one blue owns “interactive” — everything else recedes.
Palette
Deep Leaf #58a700 and Pale Sky #bbe7fc are the only shadows — as box-shadow: 0 4px 0 press edges.
Rules
- 12px radius on every button/pill/nav item, 2px borders — no gradients, no glass.
- Body stays
#777777 @ 500; only headings/fills/footer may be#58cc02. - Links/outline CTA text only in
#1cb0f6; hover is 250msease-smooth-out. - Base 4px scale: 8/12/16/24/32/40/48/64/80/96. Max content 1200px.
Fonts: Nunito 800/900 display → --font-feather, Nunito Sans 500/700 body → --font-codingo-sans.
Environment
| Variable | Default | Where |
|---|---|---|
| MONGODB_URI | mongodb://localhost:27017/codingo | backend — yes in prod |
| JWT_SECRET | — | backend — required |
| FRONTEND_URL | http://localhost:3000 | backend — CSV allowlist |
| COOKIE_DOMAIN | (none) | backend — e.g. codingo.synax.me |
| GOOGLE_CLIENT_ID | (none → 501) | backend + NEXT_PUBLIC_GOOGLE_CLIENT_ID on frontend |
| GROQ_API_KEY / GROQ_MODEL | openai/gpt-oss-20b | backend — AI primary |
| OPENROUTER_API_KEY / MODEL | llama-3.3-70b:free | backend — fallback |
| AI_DAILY_LIMIT / AI_AUTO_REPLY_DELAY_MS | 20 · 180000 (3m) | backend |
| APPWRITE_* (4 keys) | (all required together) | backend — avatar upload |
| BACKEND_URL / NEXT_PUBLIC_API_URL | http://localhost:4000 | frontend — rewrite target (BACKEND_URL server-only) |
| NEXT_PUBLIC_SITE_URL | VERCEL_URL → localhost | frontend — canonical + OG |
Setup
git clone https://github.com/user-synax/codingo.git
cd codingo
# backend — Bun 1.3.14 needs MONGODB_URI + JWT_SECRET
cd backend && bun install
# create backend/.env — see table above
bun dev # http://localhost:4000
# new shell — frontend
cd ../frontend && bun install
# optional: frontend/.env.local (NEXT_PUBLIC_API_URL etc.)
bun dev # http://localhost:3000Seed the course
cd backend
bun src/seed/seed.tsWipes Course/Unit/Lesson/Exercise then inserts JS from Zero exactly as documented above.
Build
# backend
bun run build # tsc → dist/
bun start
# frontend
npx next build
npx next startFrontend on Vercel honors BACKEND_URL. Backend on Render needs trust proxy 1 already set.
Roadmap
| Milestone | Status |
|---|---|
| Foundation — monorepo, DESIGN.md, auth, landing | Done |
| Lesson engine — 7 types, Monaco, Worker, drafts, offline queue, confetti | Done |
| Content — JS from Zero (30 lessons, 5 units) | Done |
| Gamification — XP, levels, streak+freeze, hearts, CC, badges, daily goal, leaderboard | Done |
| Community & AI — threads/SSE, hint-first Groq/OpenRouter, auto first reply, budget+cache | Done |
| Polish — PWA, legal pages, SEO/sitemap/robots/OG, 3D button edge, motion | Done |
| Launch — perf pass, moderation tooling, soft launch | Next |
Post-MVP ideas from PRD
- Second language (Python Pyodide stub exists — lazy-load planned).
- Weekly leagues, study groups, push reminders, voice tutor.
- Server-side solution verification before ranked leagues (client runner is trusted for MVP).
Want to build with this?
Read PRD.md for product intent, DESIGN.md for visual law, and docs.md for the full reference. Questions — open an issue on GitHub, or say hi at the footer of the landing page.
Last updated April 2026 · Source-checked against the running codebase. If something here disagrees with frontend/ or backend/src/, the code wins — please open a PR against docs.md.