Project status
Last updated 2026-09-06 · 24/30 capabilities live on Web · 3 surfaces
AILearn is an API-first course marketplace: one shared backend serving a Next.js web app today and a Flutter mobile app later. The public catalog, search, free enrollment, the course player with progress tracking, certificates (with downloadable, on-demand-rendered PDFs), profiles, and the full tutor panel (courses, curriculum, analytics, earnings, reviews) all run on the real database; the chess-coaching mini-app is fully live. The admin panel — site KPIs, member management (roles, tutoring, suspension), course moderation, categories, and enrollment oversight — now runs on the real database too, with an owner-vs-admin guard split; self-serve 'Become a tutor' is live. Video streaming is live too (Cloudflare Stream — tutors upload straight from the browser, a signed webhook confirms readiness, and enrolled students watch adaptive playback, verified end-to-end against a real Stream account). Quizzes are live too: tutors author multiple-choice questions per lesson, the server grades attempts (70% pass mark, capped retries), and graded quizzes gate lesson completion and the certificate behind a real pass. The student quiz player takes them one question at a time — a reusable, mobile-style runner (progress bar + graded score card, split into components ready for the Flutter surface); pnpm db:seed-quiz fills the demo lesson so it is playable. In-app notifications and transactional email are live: enrollments, receipts, and course completions fan out to a durable notification feed (with an unread bell) plus a best-effort email, provider-agnostic behind a zero-secret log adapter until a real email provider is chosen. Every slice is verified by a one-command test pyramid (pnpm test:all — unit tests, data audits, and real-HTTP journeys on seeded fixtures). Paid checkout is now built end-to-end on Razorpay — a student pays by UPI, card or netbanking in a popup on our own page, and only Razorpay's signed webhook unlocks the course — with the provider still swappable behind one adapter file, and prices in rupees. Real payments have gone all the way through — money captured at Razorpay, a signed webhook, and the course unlocked. The live site briefly took money without unlocking anything, because the webhook secret did not match; that is fixed, and settlement no longer rests on any single signal — if a webhook is ever refused or lost, the order page asks Razorpay directly and settles the payment the same way, and if the payment window closes or reports a failure, the page checks with Razorpay before telling you anything about your money, with the Pay button locked while it checks so a wrong 'failed' message can never lead to paying twice. Checkout now settles itself even with no browser present: a site-wide watcher re-checks the buyer's unfinished payments whenever they visit, return to the tab, or come back online (and quietly on a slow timer), a Vercel cron sweeps every site's unfinished payments every 10 minutes so even a buyer who never returns gets their course (plus receipt and push), a payment that lost its Razorpay reference in a crash is recovered by receipt, a declined-then-retried order still settles when the retry captured, and never-paid checkouts are honestly marked abandoned after 24 hours — only on Razorpay's positive say-so. Every payment is also fully investigable now: each one leaves a permanent event trail in our own database (who acted, what happened, why anything failed — outliving Vercel's one-day logs), the owner gets throttled alert emails for the scary cases (webhook refused, orphan webhook, Razorpay's API down, refunds) plus an at-most-daily digest sent only when something needs eyes, and site staff have their own Payments page in the admin panel that shows the day's health at a glance, opens any payment's full history, and asks Razorpay about it on the spot. On top of that the site owner now has a payments audit page where every payment ever made explains itself in plain words — whether it is healthy, or whether someone paid and never got access — with the next step spelled out, and our own test payments flagged so they never masquerade as a customer waiting. Anything already dealt with can be closed with a note, which quiets the daily email without hiding the record. And when a payment does need investigating, it is now one action: every row on the staff Payments page has an Investigate panel — the verdict in plain words, our status beside the provider's live answer, the webhook deliveries about that payment, and the full history, copyable as one JSON bundle — and the identical story is a single terminal command, pnpm payments:diagnose. Mobile readiness (M8) is well underway: the whole /api/v1 contract is self-describing — a machine-readable OpenAPI document at /api/v1/openapi.json (for typed Flutter/codegen clients), a human-readable reference at /api-docs, and a hard JSON-only guarantee (even unknown API paths answer structured JSON, never an HTML error page) — and the Flutter app itself now runs the core learner loop: slices 1–5 have shipped (sign-in + catalog browse, free enrollment + course player, certificates, search & filters, and profile editing), and a five-tab bottom-navigation milestone on top of them — Home/Explore, Enrolled, Wishlist, Progress, and Settings — adds a light/dark/system theme toggle, an in-app quiz player, and a saved-for-later Wishlist backed by a new GET/POST/DELETE /api/v1/wishlist, each verified against the live backend — and the web app now carries the same Wishlist (a /wishlist page plus a ♡ toggle on every catalog card and course page), so a course saved on the phone is there on the web. The phone can now BUY a course too: Buy now opens the same Razorpay checkout in the phone's browser, and an in-app Order-status screen confirms the truth from our ledger (asking Razorpay directly when needed) with the same honest wording as the web — plus a Purchase history screen in Settings and an every-app-resume sweep that rescues a purchase even if the app was killed mid-payment. That flow is green in tests and has been walked end to end on a real phone (with the no-money test provider): buying, a failed attempt retried to success, and the purchase landing in My Learning. Still open: one real-rupee payment against production, a real refund pass to close out checkout (M6), and white-label branding.
— not applicable to that surface
| Capability | Backend Shared /api/v1 + Neon DB | Web Next.js · live | Mobile Flutter · in progress |
|---|---|---|---|
Platform & accounts | |||
Sign-in & identity Firebase Auth email/password sign-in with just-in-time user provisioning on first request (replaced Clerk, 2026-07). Web has a custom sign-in/sign-up dialog + SSR session cookies; the Flutter app's session survives cold starts — verified on a real device. | Done | Done | Done |
Profile & account settings Edit your global profile (name, avatar, bio, headline) and account settings (M3). Flutter slice 5 adds an Edit profile form over PATCH /api/v1/me — name, headline, bio; the bottom-nav Settings tab adds an account section, a light/dark/system theme toggle (persisted on-device), and FAQ (M8). | Done | Done | In progress |
Roles & membership Per-site student/admin/owner ladder plus the can-tutor capability, enforced by API guards (M0). | Done | Done | Planned |
Notifications & email Enrollments, receipts, and course completions fan out to a durable in-app notification feed (with an unread bell) and a best-effort transactional email — one emit() call, provider-agnostic behind a zero-secret log adapter until a real email provider is chosen (M7). The Flutter app now consumes the same feed over GET/POST /api/v1/notifications: a bell with an unread badge in the Explore app bar (and a Settings row) opens a notifications screen with optimistic mark-read/mark-all-read reconciled to the server count, pagination, and pull-to-refresh (M8). | Done | Done | In progress |
Push notifications The same notification events also fan out to mobile push as Channel C of one emit() call — devices register their FCM token per user (device_tokens, UNIQUE-token upsert that re-homes a device to the newest signer, with self-healing prune of dead tokens), sent through a provider-agnostic seam (zero-secret log adapter by default, a real FCM HTTP v1 adapter for delivery). The Flutter app now runs on a real Firebase project: it registers on sign-in and unregisters on sign-out behind a dependency-inverted PushTokenSource seam, opens a tapped notification straight to the right screen, and shows an in-app banner when a push lands while you are using the app — while still building and testing with no Firebase config at all. Verified end to end on a real Android phone: the banner arrives, tapping it opens the right screen, and a device that uninstalls the app is dropped automatically (M8). The website now gets the same pushes: turn them on from Settings → Notifications and this browser registers as another device on the same channel — clicking a notification opens the right page on your own site's domain, and one arriving while you're already on the site shows as an in-app message instead — verified in a real browser end to end (M8). | Done | Done | Done |
Browse & discoverbrowse | |||
Course catalog & detail Browse published courses and open a course page by friendly URL or id (M1). Flutter slice 1 browses the same catalog + read-only detail against /api/v1 (M8). | Done | Done | In progress |
Search, categories & filters Find courses by keyword, category, level, and price (M1). Flutter slice 4 adds the same filters (sort/category/level chips + keyword) over a new public GET /api/v1/categories (M8). | Done | Done | In progress |
Wishlist (save for later) Bookmark courses to a personal saved-for-later list, independent of enrollment, and enroll from it later. Backend GET/POST /api/v1/wishlist + DELETE …/:courseId on Neon (idempotent; draft/cross-tenant courses answer 404). The Flutter bottom-nav Wishlist tab shipped first; the web surface now matches it — a /wishlist page plus a ♡ toggle on every catalog card and the course page, server-rendered saved state with optimistic toggling, over the same endpoints and with no backend change (M8). | Done | Done | In progress |
Enroll & payenroll → pay | |||
Free enrollment Enroll in a free course instantly and see it under My learning (M2). Flutter slice 2 adds free enrollment + a My Learning list against /api/v1 (M8). | Done | Done | In progress |
Paid checkout & payments Buy a paid course with UPI, cards or netbanking through Razorpay; the payments ledger records the fee split. Real payments have gone all the way through — money captured at Razorpay, signed webhook, course unlocked — and prices are in rupees. The live site briefly took money without unlocking anything because the webhook secret did not match; that is fixed, and the system no longer trusts any single signal: if a webhook is ever refused or lost, the order page asks Razorpay directly and settles the payment the same way, and if the payment window closes or reports a failure, the page checks with Razorpay before saying anything about your money — the Pay button stays locked while it checks, so nobody can be charged twice by a wrong 'failed' message. Checkout is now self-settling with no browser needed: a site-wide watcher settles the buyer's unfinished payments on every visit/refocus (with a full-width 'Payment confirmed' bar), a 10-minute Vercel cron sweeps all sites for the buyer who never returns, lost Razorpay references are recovered by receipt, a declined-then-retried order still settles, and never-paid checkouts are marked abandoned after 24 h only on Razorpay's positive answer. Every payment now also leaves a permanent event trail (who acted, what happened, why anything failed), the owner gets alert emails for the scary cases plus a needs-eyes-only daily digest, and site staff have their own Payments page in the admin panel — a needs-eyes summary, the ledger with buyer and course shown, each payment's full history, and an idempotent 'Ask provider' button that settles a row on the spot. Diagnosing a payment is now one action: every ledger row has an Investigate panel (the verdict in plain words, our status next to Razorpay's live answer, the webhook deliveries about that payment, the full history, and a copy-as-JSON bundle for bug reports), the same story is one terminal command (pnpm payments:diagnose — by id, order ref, buyer email, or --stuck), webhook deliveries now record what they said (and refused or ignored ones leave distinguishable traces), every API request carries a correlation id that links logs to the payment trail, and the settle cron's liveness is visible on the page. The Flutter app now buys courses through the SAME machinery: Buy now opens the hosted checkout in the phone's browser (which has no app session — so checkout sessions carry client:'mobile' and return to a new no-sign-in /checkout/mobile-return page), while an in-app Order-status screen — the only place that holds the token — polls the ledger, asks Razorpay directly after quiet polls, and shows the same honest states as the web (paid / failed / not charged / still checking, never an endless spinner); every app resume also settles any unfinished payment in one call, prices render in rupees from a new currency field on the catalog, and a Purchase history screen lives in Settings. Green in tests (mobile analyze + 324 tests, backend 56/56 payment checks) AND walked end to end on the physical phone with the fake provider — success to enrollment, and the failure branch live on the Order-status screen (failed → Try again → paid). Still to run: one real-rupee payment against production, and a real refund driven from the provider dashboard (M6). | In progress | In progress | In progress |
Learn & progresslearn → certify | |||
Course player & progress Watch lessons, resume where you left off, and track per-lesson progress (M2). Flutter slice 2 adds the course curriculum + lesson player with watch-position auto-save and mark-complete (M8). | Done | Done | In progress |
Video streaming Lesson videos transcoded and streamed adaptively via Cloudflare Stream; enrollment-gated iframe playback (M5). Flutter slice 2 plays the same HLS via video_player — natively on Android/iOS (not web), verified on a real device 2026-07-24 (ExoPlayer streams the HLS manifest and renders frames). The mobile player has play/pause, ±10s skip, a scrubbable bar with elapsed/total time, mute, 0.5×–2× playback speed and fullscreen, and stops the audio when you switch tabs; quality selection and picture-in-picture are not supported by video_player and stay open (M8). | Done | Done | Done |
Offline downloads Download lessons to watch offline (Udemy-style). A new backend endpoint hands out a single-file Cloudflare Stream MP4 rendition per lesson (GET /api/v1/enrollments/:id/lessons/:lessonId/download); the Flutter app downloads it to phone or SD-card storage (chosen in Settings), plays a completed file from disk (never blocking playback), and auto-prefetches the course's first video plus the next few as you watch — governed by an auto-download mode, a Wi-Fi-only switch, and a daily cellular-data cap. Text lessons go offline instantly. Live MP4 download + on-device file playback verify on the device track (M8). | Done | — | Done |
Completion & certificates Finish all lessons to complete a course and earn a certificate — downloadable as a PDF, rendered on demand by the API (M2, PDF M7). Flutter slice 2 completes lessons and flips a course to completed; slice 3 adds the in-app My Certificates list with authenticated PDF download (M8). | Done | Done | In progress |
Reviews & ratings Rate and review a course after 30% completion; ratings roll up on the course page (M7). The Flutter course-detail screen now shows the review list and an eligibility-gated write/edit/delete form over the same GET/PUT/DELETE …/reviews[/me] endpoints (M8). | Done | Done | In progress |
Quizzes Tutors author multiple-choice quizzes per lesson; the server grades them (70% pass mark, 3 attempts per 24 h) and graded quizzes gate lesson completion — and with it the certificate (M7). Students take them one question at a time in a reusable, mobile-style runner (progress bar + graded score card); `pnpm db:seed-quiz` seeds a playable demo quiz. The Flutter bottom-nav Progress tab + in-app quiz screen take and grade quizzes over the same endpoints (M8). | Done | Done | In progress |
Lingo language lessons A Duolingo-style language engine inside AILearn: short, timed lessons made of mixed activities, with XP, stars, combos and hearts (M10). The Flutter Lingo tab now PLAYS a real lesson end to end — reveal-the-meaning cards with self-grading and token peeks, tap-the-answer quizzes with instant feedback, listen-and-type spelling (typo-tolerant), and listen-and-pick rounds — all spoken by on-device text-to-speech, with a section timer, combo meter, hearts on checkpoints, and a results screen showing score, stars and an XP breakdown (E1). It now also LOOKS the part: chunky 3D buttons that press under your finger, answer cards that flood green or shake red, a feedback sheet that slides up, a '3 IN A ROW!' burst, confetti sized to how well you did, and haptics throughout (D1). It runs on three bundled sample lessons and scores on the device; the content API, the authoring studio and server-side scoring are still to come. | Planned | Planned | In progress |
Textbook AI tutor An AI teacher that teaches from the course's own textbook (M11): the tutor uploads the book, AI pre-generates a per-chapter study guide (summary, key concepts, practice questions) that the tutor reviews and publishes, and enrolled students get that guide plus a live chat grounded in the chapter — citing pages, giving hints before answers, refusing questions the book doesn't cover — fronted (later) by an animated cartoon teacher with narration. Slices 1–4 are LIVE: a tutor attaches a PDF (private R2 upload; page-anchored chapter split tuned on real Samacheer books), generates each chapter's study guide with AI and reviews/publishes it in the Book page — nothing AI-written reaches a student unreviewed — and enrolled students open the AI Tutor from their course page: a study guide with hint-first practice questions, plus a live chat that answers only from the chapter, cites the exact pages, and stops at a per-student daily message cap. The AI also keeps a short memory note per student (refreshed every few turns) so tutoring picks up where it left off — and because students are minors, the human tutor can read every conversation AND that memory note in a read-only Student conversations tab. The tutor is now a visible character: Asha, an original cartoon teacher who thinks, talks and encourages on screen, speaks each chapter aloud from pre-generated narration (her mouth moves to the real audio), reads chat replies out loud, and takes spoken questions through the mic. A per-book eval harness keeps her honest. Students can also now READ the book inside the app: the real pages render in the browser with a tappable highlight over every detected section — tap one and its text opens beside the page with an 'Ask Asha about this section' button that pre-fills the chat (plus a plain text view as toggle/fallback). Access stays enrollment-gated behind short-lived links; the tutor asserts the right to showcase the book by uploading it. Any PDF now works, scanned or digital: every page is classified on its own, pages with no usable text are read from their image by the AI and checked by an independent second AI read, every answer is stored once per file and reused forever, and when two reads disagree a third read settles it — the tutor never has a review queue, only an optional glance at the rare page the AI settled alone. The reader's section list now comes from the AI's reading of every page (a list item or an exercise question is no longer mistaken for a heading), and students see the book's own printed page numbers ("Book Pg 82"). Design docs: docs/book-tutor/; mockup at /dev/book-tutor. | In progress | In progress | Planned |
Chess coaching Standalone chess mini-app: play the engine, analyse positions, solve puzzles, ask the AI coach (shipped side feature). | Done | Done | — |
Teach & earnteach → earn | |||
Become a tutor Self-serve tutor opt-in (or owner-granted), gated per site by a feature flag (M4). | Done | Done | — |
Course authoring Create a course, build the curriculum (sections and lessons), set the price, and publish (M3). | Done | Done | — |
Video upload Tutors upload lesson videos straight to Cloudflare Stream from the browser; a signed webhook confirms readiness, and an on-demand "Refresh status" reconciles it directly from Cloudflare when the webhook can't reach the machine (M5). | Done | Done | — |
Course thumbnails Tutors set a course cover image — upload it straight to Cloudflare R2 via a one-time presigned URL (bytes never touch our server) or paste a hosted image URL; shown on course cards. | Done | Done | — |
Students & analytics See who's enrolled per course, their progress, and completion analytics (M3). | Done | Done | — |
Earnings & payouts Per-course revenue, fee split, and payout history for tutors (M3, real money in M6). | Done | Done | — |
Operate | |||
Site administration Admin panel: manage members (roles, tutoring, suspension), moderate courses, categories, enrollment oversight, and site KPIs — owner-vs-admin guard split (M4). | Done | Done | — |
Branding & white-label Each buyer's site runs on its own domain with runtime, DB-backed branding (M9). The Flutter app already picks its tenant at RUNTIME — the site key it sends as X-Site-Key moved from a build-time flag to a persisted setting (Settings → Developer), so one binary can point at any site; the switcher is a development-stage affordance to be gated before a real-user release (M8). | In progress | Planned | In progress |
Platform console Superadmin creates sites, assigns owners, and sets plans/limits across the platform (M9). | Planned | Planned | — |
API contract & docs The /api/v1 surface is self-describing: an OpenAPI 3.1 document served at /api/v1/openapi.json (drives typed mobile/codegen clients), a human-readable API reference at /api-docs, and a structural JSON-only guarantee — every response, including unknown-path 404s, is the standard JSON envelope. Deep-link readiness is also underway: .well-known/assetlinks.json + apple-app-site-association serve the Android/iOS Universal-/App-Link contract (placeholder identity values pending real app signing) — see docs/04-backend-api/deep-links.md (M8 mobile readiness). | Done | Done | In progress |
Automated testing & verification One-command test pyramid (pnpm test:all): unit tests, invariant audits, domain checks, and real-HTTP user journeys on seeded fixture tenants — plus dev-only /dev pages for click-to-check browser verification of integrations like Cloudflare Stream (ongoing, spans all milestones). | Done | Done | — |
Try what's live
Jump from the "Done" claims above to the real thing.
Deferred (post-MVP)
Intentionally out of scope until the core loop is solid.