Free foreverDuolingo for codeNext.js 16 · Bun · MongoDB

Codingo docs

Learn to code. Free. Fun. Together. — bite-sized lessons, real code in your browser, and a community that gets you unstuck. This page explains how the product works and how the codebase is put together.

New here? Start at the landing page or jump straight to sign up. Already learning? Your path lives at /app/learn.

30 lessons 7 exercise types Hearts + CC + streak

At a glance

Audience
Beginners 15–25, first language JS
Lesson length
2–5 min · 5–8 exercises
Hosting
Vercel + Render + Atlas
PWA
Offline + installable

Need the markdown? It is the same source at docs.md in the repo root — this page is its human wrapper.

Overview

What it is

Codingo 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

1

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.

2

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).

3

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.

UnitLessons
Unit 1 — FundamentalsWhat is Code? · Variables · Types · Operators · Template Literals · Checkpoint
Unit 2 — Control FlowIf / Else · Comparisons & Logic · Switch & Ternary · For Loops · While Loops · Checkpoint
Unit 3 — Functions & ScopeFunctions Basics · Parameters · Scope · Arrow Functions · Callbacks Intro · Checkpoint
Unit 4 — Data StructuresArrays Basics · Array Methods · Objects Basics · Strings & Arrays · Errors & Debugging · Checkpoint
Unit 5 — Async & ProjectDOM 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.

Hint per exerciseInstant right/wrong + explanationExit → save draft → resume

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

First Lesson5 LessonsPerfect (100%)3-Day StreakWeek Warrior (7)Night Owl (after 10pm)

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 code only.
  • 20 per day per user (AI_DAILY_LIMIT), counted in AiUsage. Cache hits in AiCache (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 user codingo-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 · requireDb

Monorepo

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.

RouteFileAuth
/app/page.jspublic → redirects if authed
/login · /signup · /forgot-password(auth)/*/page.jsguest (AuthLayout)
/onboardingonboarding/page.jsauthed + !completed
/appapp/app/page.jsdashboard
/app/learn · /app/learn/[lessonId]app/app/learn/**path + runner
/app/community · /app/community/[threadId]app/app/community/**feed + detail (SSE)
/app/leaderboardapp/app/leaderboard/page.jsglobal board
/app/profile · /app/settingsapp/app/**self
/u/[username]app/u/[username]/page.jspublic showcase
/docs · /privacy · /terms · /policy · /offlineapp/docs|privacy|…/page.jspublic

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 queue
  • lib/runner — Worker JS sandbox + compareOutput

PWA + perf

  • public/sw.js (must-revalidate) + RegisterSW + InstallApp
  • site.webmanifest + sitemap.js + robots.js + OG JSON-LD on /
  • Images correct/wrong/complete.mp3 and 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 leaks solution).

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

  • progress key userId:lessonId — authoritative docs
  • meta userId — cached {xp,level,streak,badges,lastSync}
  • pending …:ts:rand — offline queue drained when back online
  • lesson_drafts userId:lessonId — runner resume (idx + answers + checked + firstTryCorrect)

Module: lib/progressDb.js · store: stores/progressStore.js (hydrate → fetch → merge pending → syncPending).

Save flow

  1. Optimistic write to memory + putProgress instantly.
  2. POST /api/progress — on success replace with server doc; on fail push to pending and keep optimistic.
  3. syncPending on reconnect drains pending; new fetches never clobber pending completions.
  4. 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.mdapp/globals.css @theme. One saturated green owns “correct/progress”, one blue owns “interactive” — everything else recedes.

Palette

#58cc02#d7ffb8#1cb0f6Charcoal #4b4b4bPencil #777Faded #afafaf

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 250ms ease-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

VariableDefaultWhere
MONGODB_URImongodb://localhost:27017/codingobackend — yes in prod
JWT_SECRETbackend — required
FRONTEND_URLhttp://localhost:3000backend — 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_MODELopenai/gpt-oss-20bbackend — AI primary
OPENROUTER_API_KEY / MODELllama-3.3-70b:freebackend — fallback
AI_DAILY_LIMIT / AI_AUTO_REPLY_DELAY_MS20 · 180000 (3m)backend
APPWRITE_* (4 keys)(all required together)backend — avatar upload
BACKEND_URL / NEXT_PUBLIC_API_URLhttp://localhost:4000frontend — rewrite target (BACKEND_URL server-only)
NEXT_PUBLIC_SITE_URLVERCEL_URL → localhostfrontend — 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:3000

Seed the course

cd backend
bun src/seed/seed.ts

Wipes 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 start

Frontend on Vercel honors BACKEND_URL. Backend on Render needs trust proxy 1 already set.

Roadmap

MilestoneStatus
Foundation — monorepo, DESIGN.md, auth, landingDone
Lesson engine — 7 types, Monaco, Worker, drafts, offline queue, confettiDone
Content — JS from Zero (30 lessons, 5 units)Done
Gamification — XP, levels, streak+freeze, hearts, CC, badges, daily goal, leaderboardDone
Community & AI — threads/SSE, hint-first Groq/OpenRouter, auto first reply, budget+cacheDone
Polish — PWA, legal pages, SEO/sitemap/robots/OG, 3D button edge, motionDone
Launch — perf pass, moderation tooling, soft launchNext

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.