IELTS Guru is FLYER's AI-first IELTS preparation product: skill-by-skill mock tests with real exam simulation, AI grading for Speaking and Writing at a fraction of the real exam cost, an AI Mentor for chat and voice practice, and a diamond + subscription monetization model. This PRD documents 100% of the shipped product β every route, flow, business rule, data entity and known gap β from a direct scan of the flyer/webs-ielts codebase plus fresh production-database numbers.
Version
1.0
Date
24/08/2026
Owner
TΓΉng (Product)
Status
Live in production
π Production (24/08/2026)207,767 IELTS attempts all-time28,195 learners all-time11,612 attempts / 30d661 active learners / 30dlive since 22/10/2024
π Content1,789 parts3,550 questions4 skillscounts from the production read replica, tables ielts_*
IELTS Guru targets learners preparing for IELTS who want unlimited realistic practice and instant expert-level feedback without paying for the real exam or a private tutor. Positioning line: "AI Mentor for smart IELTS preparation" β the product claims AI Speaking/Writing grading at 1/50 the cost of the real exam. It serves three audiences at once: B2C self-study learners (Vietnam), international B2C learners (USD pricing via Polar), and B2B school tenants through white-label custom domains (including a dedicated ACCA tenant at acca.teado.ai).
Brand & domains: the UI logo and SEO title say "IELTS GURU", but the codebase still canonicalizes every page to ielts.flyer.us; ieltsguru.ai serves the same app (its subdomain is on the first-party allow-list). Per-page SEO titles still carry the older name "FLYER IELTS Virtual Testing Platform" β a branding inconsistency to resolve (Β§13).
Repo: standalone flyer/webs-ielts on Gitea (split from the flyer/webs monorepo with history preserved). Branch dev β dev env, main β staging, tags v*.*.* β production (currently v1.36.x line).
Deploy: Docker (arm64) β ECR β EKS cluster flyer via Tanka/jsonnet; production HPA 2β5 replicas at 80% CPU; health checks on /api/ping; CI on Gitea Actions with Mattermost notifications.
Companion apps: native iOS/Android app com.flyer.ielts (App Store id 1671502218) wired via universal links; teacher side lives in teacher.flyer.us (assignments, review); payments execute on exam.flyer.us checkout (VN) or Polar (US).
Goals. (1) Give IELTS learners unlimited exam-realistic practice per skill. (2) Replace expensive human grading with AI band scoring + actionable feedback. (3) Feed the FLYER ecosystem: shared accounts, teacher assignments, school exams, band-tiered subscriptions.
2Users & user stories
Role
User story
Priority
Self-study learner
Take a full simulation mock test for one skill under real exam conditions (timer, play-once audio, anti-cheat) and get a band score.
P0
Self-study learner
Get AI Writing feedback with the official 4-criteria band breakdown, sentence-level corrections and a sample answer.
P0
Self-study learner
Get AI Speaking feedback with transcript, per-word pronunciation (IPA, phonemes) and fluency/grammar/vocab scores.
P0
Self-study learner
Practice flexibly: pick specific parts, set my own time limit, redo questions, use dictionary/notes while practicing.
P0
Learner
Talk to an AI Mentor β free-text chat or voice Topic Talk with learning outcomes β to practice English conversationally.
P0
Learner
Track my progress: band trend over time, skill radar, accuracy, study time, target band and days until my exam.
P1
Learner
Review mistakes: wrong answers with explanations, mispronounced words I can re-record and re-score, a vocabulary book.
P1
School student
Enter a proctored multi-skill school exam session assigned by my school, with violations reported to the teacher.
P0 (B2B)
Teacher (via teacher.flyer.us)
Assign tests as missions with deadlines, hide answers, review and grade student attempts manually.
P1
Anonymous visitor
Browse all test libraries and even take tests with an anonymous profile before being asked to sign up.
P1 (funnel)
3Information architecture & routes
Single left-sidebar app shell (no footer). Locales: en (default, unprefixed) and /vi. Auth gating is soft: no middleware or server-side gate β anonymous visitors get a dismissible "please sign up" toast everywhere except a small exclude list, and the sidebar intercepts clicks on member-only items to open the sign-in modal.
Route
Purpose
Access
/ β /home
Dashboard: greeting, "Personal AI Teacher" free-text box (creates AI chat session), skill cards with mock-test/test counts, Speaking Topics entry, upcoming school-exam popup.
Public
/libraries
Merged material library. Two rows: Study (FLYER-curated IELTS practice) and Quiz (community/partner practice). Tabs All / FLYER / Community / My school; URL-persisted filters (search, skill, band, type, sort).
Public
/ielts/{skill} Γ4
Mock-test list per skill (listening, reading, writing, speaking) with topic/type/sort/part filters and infinite scroll.
Public
/ielts/{skill}/[id] Γ4
Test detail + player in one route: preview β mode choice β (sound/mic check) β do exam. SSR for SEO meta.
Public β gated to start
/ielts/{skill}/result/[id] Γ4
Score screen after submit; polls every 3s while AI grading is pending.
Sidebar nav (desktop sidebar = mobile drawer): Mock tests β /, Study β /libraries, Missions, Mistakes, Vocabulary book, Learning profile, History; bottom block: Teacher role (opens teacher.flyer.us), Notifications popup, diamond counter β /diamonds-preview, avatar popup (Upgrade β /packages, Change interface, Active code, Switch profile, Edit profile, Log out).
4Content model & inventory
All content lives in the universal CheckPoint container shared across FLYER products, with per-skill IELTS part edges. Rendered question formats (MCQ, T/F/NG, matching, headings, form completionβ¦) are all composed from just 3 detail primitives, driven by <blank id> placeholders inside contentHtml:
GAP_FILL β free-text inline input, autosaves on blur. SELECT β radio, or multi-checkbox when the answer key has >1 correct option. OPTION β inline dropdown for matching/heading types.
The separate quiz engine (community practice) supports 13 part types: multichoice, gapfill, dropdown, match, drag-drop onto image, word ordering, reorder, flashcard, AI-evaluated open-ended, passage, interactive YouTube video, audio response (Γ2).
Sub-checkpoints (sectionCheckPoints) let a single Speaking part be sold/attempted separately with its own diamond price.
Production content & usage inventory (read replica, 24/08/2026)
Skill
Parts
Questions
Part attempts
Question attempts
Listening
488
857
96,011
165,128
Reading
461
1,258
60,770
150,434
Speaking
342
1,361
37,917
149,311
Writing
498
74
38,585
4,122
Total
1,789
3,550
233,283
468,995
Writing has few "questions" because most writing tasks are part-level essay prompts, not question-level items. In-test notes saved by learners: 3,564 rows (ielts_notes).
5Mock test & practice flows
Two modes on every test
Practice mode
Simulation mode (mock test)
Parts
Learner picks any subset (Speaking: exactly one part)
All parts, fixed order
Timer
Learner picks 15/30/45/60 min
Ξ£ part times, auto-submit at 0
Audio
Free seek/pause/replay
Plays once, no transport controls; Speaking prompts have a replay budget of 2
Question text
Visible per content setting
Speaking: hidden except Part 2
Tools
Dictionary, translate, highlight, notes
Dictionary/translate disabled; copy/paste blocked
Anti-cheat
β
Forced fullscreen; tab-switch/fullscreen-exit counted as violations, warning modal, extension popups hidden; violationCount submitted with the attempt
Per-skill session flow
Listening / Reading: Preview (mode cards + diamond cost + AI-vs-teacher scoring choice) β sound check (Listening only) β exam screen with react-hook-form state, 1-second countdown, question palette per part, Howler audio player (Listening) or resizable passage/questions splitter (Reading) β every answer autosaves via submitPartAttempt β Submit or timeout β endAttempt(violationCount) β result page.
Writing: Preview with scoring-method radio (Teacher β only if canRequestTeacher β vs FLYER AI) β split view: prompt + images left, essay textarea right with live word count (soft warning at 500 words, hard cap with toast) β autosave on blur β submit β result polls while AI grades.
Speaking: Preview (single-part practice or full simulation) β speaker check + mic check (local-only recording) β per part: typewriter intro β per question: prompt audio β think countdown (timeToThink) β recording auto-starts, ring fills over timeToSpeak, auto-stop β upload (RecordRTC mono 16 kHz β /record β recordId) β submit per question (+ optional note in simulation) β part timer can force-submit in-flight answers β end attempt β result.
School exam sessions (B2B, feature flag student_school_exam): a blocking popup lists the exam's per-skill sessions with status pills; "Enter exam" opens the test with ?examId&sessionId&examMode=1 β Practice mode is hidden, the attempt is linked to the session via REST (link-attempt), and violations are additionally reported live to POST /v3/learner/exams/cheats.
Band scoring. Computed server-side. Full test β band 0β9 displayed as X.X/9.0 (report side rounds to nearest half band); partial/practice β raw correct/total instead of a band; Speaking prefers teacherGrade over AI grade when both exist. There is no combined 4-skill sitting inside the app β multi-skill mock exams are orchestrated as school exam sessions.
6AI grading β Writing & Speaking
Writing
Trigger: learner chooses "FLYER AI" scoring at start (requestAIReview); costs the check point's diamond price. The client deliberately never pre-checks the wallet β the backend answers NOT_ENOUGH_DIAMOND (it also counts free quota).
Result: report polls every 3 s until graded. Two report generations: legacy (4 criterion tiles + markdown commentary) and V2 whenever detailedFeedback exists.
V2 rubric mirrors the official band descriptors β Task Achievement/Response, Coherence & Cohesion, Lexical Resource, Grammatical Range & Accuracy β each broken into sub-criteria (e.g. paragraphing, cohesive devices, collocation, punctuation control), every leaf a bilingual "Feedback + How to improve" pair with a machine key mapped to a severity badge.
Sentence-level corrections:sentenceSuggestion[] underlines flagged sentences in the learner's essay, color-coded by worst errorImpact (CRITICAL/SEVERE/MODERATE/MINOR/NONE), with diff popovers, a vocabulary-suggestion list, and scroll-synced spotlighting. Sample answer shown alongside.
Export: Word (docx), PDF (print), shareable link. Teacher path supports quoted inline comments, audio comments, fixed answer and a criteria table.
Speaking
Pipeline: recording β upload β async grading after end-attempt (Azure Speech assessment + LLM); the result page polls every 3 s with a 2-minute deadline, then degrades gracefully.
Attempt level: overall band + 4 progress bars (pronunciation, fluency, vocab, grammar), each with a correct/warning/incorrect decision color.
Per question: learner audio playback, transcript, suggested answer (HTML + audio), skill chips. Pronunciation feedback renders per word: IPA, nativeness score, per-phoneme accuracy breakdown, TTS of the correct pronunciation next to the learner's own audio. Azure payload additionally carries a Cambridge assessment (grammar/vocabulary scores, mistakes EN/VI, suggestions).
Quota: learners get a weekly free AI Speaking allowance (aiIeltsSpeakingQuota remaining/total/used) before diamonds are charged. Advertised diamond rates: from 2π per AI mini test, 25π per AI full test.
A richer DetailedFeedbackSpeaking schema (band-descriptor breakdowns mirroring Writing V2) already exists server-side but is not yet consumed by the web client (Β§13).
7AI Mentor (Bingo)
Entry: the Dashboard's "Personal AI Teacher" textarea (12,000-char cap) creates a chat session and hands the first message over; or the sidebar hub at /real-time-conversation with two mode cards: Chat and Topic Talk.
Chat (/real-time-conversation/chat/[id]): SSE-streamed markdown answers through an edge API route proxying the backend chat-AI endpoint, with conversation history rail, suggestion chips, infinite scroll, switch-to-call affordance.
Topic Talk (/real-time-conversation/topic): browse Bingo subjects (level, topic, attempts, score; sortable), then a voice conversation: learner records, audio uploads and attaches to the message, and the reply streams back as interleaved text chunks + base64 MP3 audio + viseme frames driving a lip-synced Rive avatar, played through an ordered audio queue. Each subject carries a learningOutcomeSubjects[] checklist ticked off live as the learner covers each point; sessions complete explicitly.
Limits: daily voice-call quota ("You have used up your call limit for today"). A third "Free talk / Call Bingo" mode is translated and partly implemented but not wired into the mode chooser (Β§13).
Supporting AI endpoints: reply suggestions, TTS (textToSpeak), dictionary (EN-VI/EN-EN/synonyms with Google Translate + MyMemory fallbacks), open-ended quiz auto-evaluation, mistake-word pronunciation re-scoring.
8Progress, reports & study tools
Tool
Behavior
Result screen
Band or raw score with confetti; four states: scored / waiting-AI (Rive loader, 3s poll) / waiting-teacher / no scoring requested (offers "Redo with FLYER AI"). L/R auto-opens an inline answer review; hidden entirely when the assigning teacher set isHiddenAnswer.
L/R detailed report
Score side: correct/total, time spent, half-band score, two-column answer-key grid (learner vs correct, joined by "or"). Detail side: per-part audio + toggleable tapescript (Listening), read-only questions with β /β, explanation accordions (EN/VI).
History
Cursor-paginated table with debounced search and skill/type/status filters; each row deep-links to the correct result/report route in a new tab with ?url_back. Badges for challenge codes and placement tests.
Learning profile
Period selector (today β all time); 6 stat cards (completed tests, study time, accuracy, target band, exam date, days-to-exam); average-band line chart; 4-skill radar chart with "needs improvement" label; target modal validates .0/.5 band steps and entry < target.
Mistakes drill
Mispronounced words with model TTS vs the learner's own recording; re-record and re-score (per-phoneme IPA feedback) under a re-scoring quota; CEFR-level and status filters.
Vocabulary book
Notes grouped by check point, editable definitions, TTS, created from the in-test highlight tooltip (Note / Highlight / Dictionary on any selected text in passages).
9Accounts, auth & multi-tenant
Login methods: phone-or-username + password, and Google via Firebase. No Apple/Facebook; email is a profile field, not a login. Password recovery by OTP.
Signup: phone β OTP β password (modal flow; legacy full-page also live). Dual OTP transport: backend SMS OTP for VN numbers, Firebase phone OTP + invisible reCAPTCHA for international; Cloudflare Turnstile on top. ?ref= captures white-label source domain.
Multi-profile: one FLYER account holds up to 20 learner profiles; switcher at /profiles; anonymous visitors get a cookie-based anonymous profile so they can attempt tests pre-signup. A cross-app profile_id cookie on the apex domain keeps exam/study/ranking/speaking apps in sync.
Sessions: cookie-based Apollo with device: WEB_IELTS; single-session enforcement β logging in elsewhere triggers a live GraphQL-subscription event and a blocking "you were logged out from X" toast.
Settings: Profiles tab (avatar upload, name, DOB, gender, province/school/class join, read-only email/phone, subscription expiry, change password) and Theme tab (light/dark + 8 accent colors, persisted pre-hydration; English/Vietnamese switch). WCAG-AA contrast is enforced by a custom lint gate on the token file.
White-label: any non-first-party subdomain is a tenant; auth routes bounce to the main domain and back; per-domain flags hide diamonds or lock name changes; the ACCA tenant gets a placement lead form (/join-placementest) and suppressed sign-up nags.
Notifications: in-app popup only (badge count, challenge invites); no push/email preference center.
10Monetization & premium gates
Subscription plans (FLYER IELTS PRO)
Plan
VN price (VND)
VN list
US price
Checkout
3 months
790,000
1,000,000
$49
VN β hosted checkout on exam.flyer.us (VNPay); US β Polar checkout (hardcoded product IDs); default selection = 6-month
6 months β
1,550,000
2,400,000
$89
12 months
2,320,000
3,900,000
$149
Diamond top-up (AI-grading currency)
Pack
Diamonds
Price (VND)
Trial
25
25,000
Basic
50
50,000
Pro Learner β
125
99,000 (was 125,000)
Wallet = diamond (purchased) + freeIeltsDiamond (rewarded), shown with a breakdown popover. Third monetization channel: activation codes redeemed at /active-account (grants plan days + diamonds, with redemption history).
Band-range entitlement β the subscription tier carries bandFromβbandTo; the band filter and visible catalogue are scoped to that window (legacy packages = all bands 0β10).
Diamonds per attempt β AI grading for Speaking/Writing charges the check point's diamond price; enforcement is server-side (NOT_ENOUGH_DIAMOND).
Teacher review β the "Request scoring from teacher" option appears only when the learner's profile has canRequestTeacher.
Attribution: UTM cookies (24h TTL), referral_code (member-get-member, capture only β no referral UI), ?ref source domain into signup. Analytics: GTM + GA4, Firebase Analytics, Meta Pixel; Sentry for errors. iOS smart-app banner + universal links to com.flyer.ielts.
11Data, API & infrastructure
API surface
GraphQL v2 (primary; ~80 queries, ~62 mutations, 1 subscription; all inline codegen documents): content catalogue, the symmetric per-skill attempt lifecycle startAttempt β submitPartAttempt (per part/question) β endAttempt(violationCount), results/reports with grading fragments, auth/profile, notifications, missions, AI-conversation sessions, notes/dictionary/mistake words, stats. The single subscription pushes session-invalidation events.
REST v3 ("Flyer EdTech Platform API", Rust backend; 242 generated hooks but only 4 used): feature configs, learner exams, link-attempt (bind attempt β school exam session), cheat reporting. Escape-hatch hooks exist for the remaining 182 paths.
Edge SSE proxies for AI chat/call streaming; audio uploads via a REST record endpoint; speech-to-text via a v2 REST endpoint.
Key entities
CheckPoint (universal content container, ~80 fields) β per-skill parts β questions β 3-primitive details; UserAttempt (~55 fields β one attempt entity for every FLYER product, with per-skill part attempts, band/score fields, AI + teacher grading, violation count, gamification earnings); User (wallet, subscriptions + band entitlements) β up to 20 LearnerProfile (target band, exam date, streak, AI speaking quota, school/class, custom domains). Production tables: 22 ielts_* tables on the read replica (parts/questions/attempts per skill + notes).
Infrastructure
Gitea Actions CI β Docker arm64 β ECR β EKS via Tanka/jsonnet; dev (dev branch), staging (main), production (version tags). HPA 2β5 replicas at 80% CPU with documented scaling rationale; health probes on /api/ping.
Custom imgproxy pipeline for images (human-readable URLs, 307 fallback to Next image when imgproxy is down); media on statics.flyer.us.
Sentry self-hosted (init lives in the Apollo module; sampled 10%/1%); GA4 + GTM + Meta Pixel + Firebase Analytics; Remote Config as a second feature-flag channel next to /v3/learner/feature-configs.
Engineering conventions worth keeping: oxlint/oxfmt, knip, a hardcoded-hex ratchet budget and a WCAG-AA contrast gate as custom lint scripts, heavily documented config files ("comments are the documentation").
12Edge cases & error handling
Situation
Handling
Not enough diamonds at start
Backend returns NOT_ENOUGH_DIAMOND; modal routes to upgrade (currently /packages β see gap #5).
Attempt errors generally
A 16-code taxonomy over two error channels, each mapped to toast-vs-modal, i18n key, and an action (login / result / diamond / back / restart / retry); intrusiveness depends on phase β start blocks, submit mostly toasts, end always offers an exit.
Timer reaches 0
Auto-submit; Speaking additionally stops the recorder, force-submits the in-flight answer with a 20 s safety net.
Leaving fullscreen / switching tabs in simulation
Violation counter + warning modal + title flash; ESC swallowed and fullscreen re-entered; count rides on end-attempt and (in school exams) live cheat reports.
Logged in elsewhere
Live subscription event β blocking "logged out from X" toast with re-login.
Abandoned speaking question
An upload-generation guard drops late audio uploads belonging to a question the learner already left.
AI grading slow/failed
3 s polling; Speaking gives up gracefully after 2 minutes; Writing keeps a skeleton until feedback arrives.
Teacher hid answers
isHiddenAnswer suppresses all answer review and redirects the review route home.
Anonymous user on member pages
Sidebar intercepts with sign-in modal; direct URLs render empty shells (queries skipped) rather than redirecting.
imgproxy down
307 fallback to the built-in Next image optimizer, no broken images.
13Known gaps & technical debt
Found during the full-codebase scan β each is either a product decision to make or a fix to schedule.
#
Finding
Type
1
No placement test, onboarding questionnaire or generated study plan. "Personalized roadmap" exists only in the SEO description; placementLevel, onboardingInfo and the STUDENT_ONBOARDING flag are data-model-only. Target band is captured but never drives recommendations.
Product gap
2
Vietnamese is force-disabled at runtime: both branches of the IP check call setLanguage("en"), overriding even manual choice on next load.
Bug
3
Anonymous pricing is wrong for international visitors: isFlyerUs returns false for all logged-out users, so US visitors see VND prices and the VN checkout.
Bug
4
No Polar path for diamonds β international users can buy a subscription but cannot top up diamonds (AI grading currency) at all.
Product gap
5
NOT_ENOUGH_DIAMOND routes to /packages (subscriptions) instead of /diamonds-preview (diamonds).
UX
6
No delete-account flow (orphan error string only) β App Store 5.1.1(v) compliance risk given the linked iOS app.
Compliance
7
DetailedFeedbackSpeaking (Writing-V2-style band-descriptor Speaking feedback) exists in the schema but is not consumed by the web client.
Roadmap
8
"Free talk / Call Bingo" mode is translated and partly implemented but not wired into the conversation mode chooser; /v3/learner/journey/continue generated but never called.
Roadmap
9
Brand/domain split: canonical URLs, robots.txt, sitemap and Polar success URL all point at ielts.flyer.us; per-page SEO titles still say "FLYER IELTS Virtual Testing Platform". A migration to ieltsguru.ai touches ~7 known files.
Brand
10
Sitemap is 12 hardcoded static URLs β no dynamic test detail pages, no /packages; dynamic generation is commented out.
SEO
11
Committed secrets: live Polar tokens, Firebase config, Redis password and other keys sit in the committed .env* files β rotate and move to a secret store.
Security
12
Premium redirect only fires for logged-in users β the subscription paywall is bypassable by logging out (anonymous attempts allowed by design).
Decision
13
Dead routes still referenced (challenge screens, /verify-account, /update-profile, /checkoutβ¦), a disabled phone field wired to console.log, half-wired daily reward, settings tabs (general/notifications/privacy) translated but unbuilt, retired GAMIFICATION theme = large unreachable code tree.
Debt
14
Learner feedback goes to a Google Sheet via Apps Script (bug-report widget), not into the product database; Zalo is the support channel.
Ops
14Acceptance criteria & future
Acceptance criteria (verified against production, 24/08/2026)
All four skills offer both Practice and Simulation modes with the mode differences in Β§5; simulation enforces fullscreen anti-cheat and submits violationCount.
AI Writing report returns the 4-criteria band breakdown with bilingual sub-criteria feedback and sentence-level corrections; AI Speaking report returns transcript + per-word pronunciation with IPA/phonemes; both poll at 3 s.
Diamond charging is enforced server-side including free quota; wallet = purchased + rewarded diamonds.
Band-tier subscriptions scope the visible catalogue via bandFromβbandTo entitlements.
School exam sessions hide Practice mode, link attempts to sessions, and report cheats live.
Anonymous visitors can browse and attempt tests on an anonymous profile; one account supports up to 20 learner profiles with single-session enforcement.
Production numbers on this page reproduce from the read replica (ielts_* tables; usage = user_attempts.type_attempt='IELTS').
Recommended next (from the gaps)
Close the loop on personalization: placement test + onboarding β use ieltsTargetScore/ieltsExamDate to actually recommend content (the "roadmap" the marketing already promises).
Ship Speaking feedback V2 (schema already live) and the Free-talk voice mode.
International revenue fixes: Polar for diamonds, correct anonymous geo-pricing, delete-account flow for app-store compliance.
Brand consolidation on ieltsguru.ai: canonical/sitemap/robots/OG + per-page titles, plus dynamic sitemap for test pages.
Security hygiene: rotate committed secrets, move to the k8s secret store.
β PRD catalogue Β· Source: direct scan of flyer/webs-ielts branch dev (24/08/2026) + FLYER production read replica Β· Internal β FLYER only.