- EdTech · LMS
- Production
- 3K+ users
Edly
Proposed and built solo the dictation practice and the pre-exam warm-up, and designed module-based assignments and AI question analysis, on an LMS with 3,200+ users and a 61,000-question bank.
Visit the live site (opens in a new tab)- Role
- Full-stack · led 2 developers
- Team
- 3–5 people
- Timeline
- 03/2026 – 09/2026
- Status
- In production
3.2K+
Registered users
61K+
Questions in the bank
2
Features I proposed and built solo
Laravel
Vue 3
TypeScript
Inertia.js
MongoDB
- OpenAI

Context
Edly (edly.vn) is a Protean Studios EdTech / LMS platform, built partly on the Prep4u codebase: prep for all four IELTS skills, the Digital SAT and university aptitude tests, plus video courses, online classrooms for teachers and mini-games. Its users are students, teachers, parents, sales staff, content editors and admins.
The codebase is large and runs two frontend stacks side by side — Inertia + Vue 3 with TypeScript for newer areas and Livewire for the CMS and older pages — with content data in MongoDB and transactional data in MySQL. I worked on it from March to September 2026.
Problem
- Existing IELTS Listening tests were only used for exams. Students had no way to practise listening actively, sentence by sentence, and the product lacked a free channel for organic traffic.
- Students entered the exam room cold. A short warm-up was needed before each test — without lowering the share of students who actually start it.
- Assigning SAT work to classes had to respect scope: admins share tests with teachers per module, and teachers may only assign the modules they hold.
- The 61,000+ question bank needed transcripts, difficulty levels and categories, far too many to do by hand.
My Responsibility
- Dictation practice — 100%, my own proposal: idea → backend and APIs → MongoDB data model → Vue UI → progress tracking → AI translation and vocabulary → SEO and SSR → release.
- Warm-up mini-game — 100%, my own proposal: business rules → database → APIs → frontend game integration → tests → documentation → release.
- Module-based assignment: analysed the requirements and designed how tests are shared and assigned per module.
- AI question analysis with OpenAI (GPT-5.5) — 100%: transcripts, difficulty levels and categories.
- Contributed to the SAT exam-room logic, Classroom / LMS, several IELTS modules, analytics and the CMS, and optimised APIs, queries and caching in the flows I owned.
- Led a group of two developers: splitting tasks, reviewing code and designing solutions.
Constraints
- A large production codebase with two frontend stacks and data split between MongoDB and MySQL.
- Dictation had to work without signing in (it is an SEO channel) without losing progress when a user signs in halfway.
- Pages meant for search had to be server-rendered (Inertia SSR).
- Bulk AI generation is unreliable: outputs vary and long requests time out.
- The warm-up is an extra step and must not block the exam when something fails — unless the business explicitly requires it.
Architecture
- Dictation: each section of a Listening test becomes a practice lesson. The transcript is split into segments with timestamps (start – end), speaker, translation and vocabulary (word, meaning, IPA); admins adjust segments directly in the Listening test editor. An artisan command runs AI enrichment in batches. Lesson pages are rendered with Inertia SSR; progress is stored in MongoDB for signed-in users and in localStorage for guests; section data is cached, and an observer clears the cache whenever the test or its questions change.
- Warm-up:
WarmupConfig(one per test: mode, version, cooldown, blocks → items) andWarmupAttempt(status, results per block and item, first and last answers, attempts, response times) in MongoDB. The API flowsdecision → start → complete / skip; theEnforceWarmupRequiredmiddleware sits in front of the exam room; on the frontend a TypeScript mapper turns backend data into the formats the existing mini-game engine expects. - Module-based assignment: admins share tests with teachers per module → teachers assign to classes or students within the modules they hold → the system filters questions to the assigned modules and keeps each assignment's modules in sync.
- AI question analysis: OpenAI (GPT-5.5) generates transcripts, difficulty levels and categories for questions in the bank.
Key Technical Decisions
Turn existing content into a new product
- Problem: the product needed a new listening feature without producing new content from scratch.
- Decision: reuse the IELTS Listening tests — each section becomes a sentence-by-sentence dictation lesson, free and without sign-in.
- Why: content cost is close to zero, and every lesson doubles as a search landing page.
- Trade-off: lesson quality depends on the original transcript, so admins can edit segments right in the test editor.
Batched AI enrichment with rule-based quality checks
- Problem: translating and extracting vocabulary for thousands of segments with an LLM — unstable output, and long requests time out.
- Decision:
- Call OpenAI in batches of 10 segments, retry with backoff, and split requests to avoid timeouts.
- Check every translation: the Vietnamese/English length ratio must fall between 0.6 and 3.0; failing sentences are re-requested one by one.
- Detect and fix two adjacent translations that came back swapped.
- Keep only vocabulary that actually appears in the transcript.
- Run offline with
dictation:generate-metadata --dry-runto preview before writing.
- Why: LLM output is never trusted blindly; rules are cheaper and faster than reviewing everything by hand.
- Trade-off: rules catch formal errors, not wrong meanings, so admins can still edit by hand.
Guest-first without losing progress
- Decision: guests keep progress in localStorage; the API returns 200 with empty data instead of 401 so the experience never breaks; signing in halfway (auth modal + redirect) keeps the progress. Signed-in users save each sentence and in batches to MongoDB.
- Trade-off: a guest's progress is lost when browser data is cleared or the device changes — accepted, so the SEO channel has no sign-in wall.
Technical SEO for every lesson
- Decision: canonical slug URLs
/nghe-chep-chinh-ta/{slug}.htmlwith 301 redirects from old URLs; Inertia SSR with a title and description per section; JSON-LD BreadcrumbList, HowTo, FAQ, ItemList, LearningResource and AudioObject; sitemap entries, crawlability fixes and tuned og:image.
Warm-up: fail-open or fail-closed is a business decision
- Problem: the warm-up must not break the exam flow, yet some tests require it.
- Decision: three modes, each with its own failure strategy:
- Required — fail-closed: the server returns 403 on skip attempts, and the
EnforceWarmupRequiredmiddleware blocks direct access to the exam-room URL. - Recommended — fail-open: skipping is allowed, and if the API fails the student still enters the exam.
- Off.
- Required — fail-closed: the server returns 403 on skip attempts, and the
- Why: each mode is a different promise to the student; writing it into the business rules before coding kept the whole team aligned.
Versioned configuration and exact cooldowns
- Decision: one configuration per test; the version only increases when content (blocks or questions) changes, not metadata. The default 24-hour cooldown is computed as
completed_at + cooldown_hours; only completions of the same version count, while skipped or abandoned attempts (started over 30 minutes ago and unfinished) do not. A compound index{user_id, warmup_config_id, warmup_version, status, completed_at}serves exactly the cooldown query. - Trade-off: more complex attempt data, in return for students warming up again whenever the content changes.
Idempotent complete, scored on the server
- Decision: calling
completerepeatedly (flaky network, double taps) returns the same result and creates no duplicates; scoring happens entirely on the server and only counts the first answer — students may retry to learn, but retries do not score, and no pass/fail is shown.
Phased rollout with KPIs defined up front
- Decision: phase 1 was a demo on mock data reusing the existing mini-game engine; phase 2 connected the real API through a TypeScript mapper, an answer collector and a local scoring fallback. Pilot KPIs were set before launch: exam start rate may drop by at most 5 percentage points, completion ≥ 50%, skips ≤ 40%, abandonment ≤ 10%; latency targets below 100 ms for
decisionand below 300 ms forcomplete.
Assignment scoped to the modules a teacher holds
- Problem: teachers may only assign the SAT modules they have rights to, and admins need to share tests per module rather than as a whole.
- Decision: assignment rights are tied to the modules a teacher holds; the system filters questions to the assigned modules and keeps each assignment's modules in sync.
Trade-offs
- Fail-open vs fail-closed chosen per warm-up mode instead of one strategy for everything.
- First attempt vs best attempt: scoring the first answer reflects real ability, while retries still help learning.
- localStorage for guests: no barrier, but progress is tied to one browser.
- Rule-based QA for AI translations: cheap and fast, blind to wrong meanings.
- Warm-up scope in phase 1 is IELTS Reading and Listening only; the roadmap extends it to lessons, the SAT and spaced repetition.
Implementation Highlights
- Dictation experience: Easy and Hard modes, progressive hints (reveal the correct start, mask the rest), playback speed control, global shortcuts (Enter to check and move on), shadowing that only appears after the transcript is revealed, text-to-speech with the Web Speech API (0.72× slow mode), and handling for mobile audio latency.
- Dictation data: the catalogue counts segments with a MongoDB aggregation and leaves out introductions ("Speaker 0"); a star-rating and feedback modal feeds the review system and notifies admins; dictation has its own event tracking.
- Warm-up: four game formats (match pairs, quick choice, fill in the blank, listen and choose) with instant right/wrong feedback; the API
/api/v1/ielts/warmup/{decision,start,complete,skip}with proposed per-endpoint rate limits; feature tests for the decision logic and the attempt lifecycle; documentation covering business rules, the API contract, the database schema, a test guide and rollout phases; integration fixes for a missingconfigId, CSRF 419 errors and a fill-in-the-blank format mismatch.
Result / Impact
- Runs in production on edly.vn for 3,200+ registered users.
- Dictation practice is free without sign-in, and every section is a search landing page with structured data.
- The warm-up shipped with written business rules, documentation and pilot KPIs.
What I Learned
- Turning existing assets into a new product is often far cheaper and faster than creating new content.
- For bulk AI generation, rule-based QA and a dry run before writing are not optional.
- Fail-open vs fail-closed is a business decision; writing business rules and KPIs before coding keeps everyone aligned.
- Idempotency and versioning belong in the first design, especially for scoring APIs.