Skip to content
Tu Nguyen
Live

Memorilla

AI-powered English vocabulary app for Vietnamese learners, live on the App Store.

▶ Play demo
Role
Founder
Timeline
Nov 2025 – Present
Stack
  • FastAPI
  • Gemini
  • pgvector
  • FAISS
  • Supabase
  • Flutter
  • Python
  • Dart
  • sentence-transformers
  • Render
Links

Problem

Most Vietnamese learners study English vocabulary as isolated word–meaning pairs, drill them, and forget them a few days later. Memorilla is built for three groups who feel this most: busy professionals with 5–10 spare minutes a day, IELTS and TOEIC candidates who need a lot of academic vocabulary fast, and learners who have lost their foundation and want to stop rote learning.

The premise is on the landing page: “Don’t rote learn. Learn how the brain thinks.” Instead of drilling single flashcards, each review session groups related words, weaves them into a short AI-written story, and schedules the next review with a spaced repetition model. Memorilla has been live on the Apple App Store since April 2026.

What I built

  • Bilingual dictionary: a shared English–Vietnamese dictionary (IPA, meanings, examples, collocations, word forms) generated by Gemini. Entries are generated on demand only for words on a ~366k-word whitelist, then stored for every user. Search autocompletes English by prefix and Vietnamese by substring (pg_trgm).
  • Semantic review sessions: every saved word is embedded with multilingual-e5-small (384-d) from the word, its English definition and its Vietnamese meaning. Each session picks a seed word (newest, then most overdue, then least stable) and its four nearest due neighbours, so the five words reinforce each other.
  • Five-step review: Flashcards → multiple choice → AI Story → Cloze → Spelling. The backend turns the results into an FSRS rating (1–4) and reschedules each word.
  • AI Story Mode: Gemini writes a story, dialogue, email or set of micro-scenarios (under 300 words) that uses all five words, pitched one CEFR level below the learner, with a Vietnamese translation, phrasal verbs and word forms.
  • Personalized recommendations: goals and interests from onboarding are embedded into user vectors and matched against a candidate word pool with pgvector.
  • Bilingual news reader: an admin pipeline crawls an article, has Gemini rewrite it at three levels (A2, B1–B2, C1–C2) with comprehension questions, and generates Cloud TTS audio with word timestamps for karaoke-style highlighting. Tapping any word opens a lookup sheet that can save it.
  • Freemium: subscriptions through RevenueCat, with webhooks updating entitlements in Supabase.

Architecture

Memorilla architectureThe Flutter iOS app calls a FastAPI service on Render with a Supabase JWT, and uses Supabase directly for auth and profile data. The service runs an embedding model and FAISS in-process, stores data in Supabase Postgres with pgvector, and calls Gemini and Google Cloud TTS. RevenueCat webhooks reach a Supabase Edge Function.Flutter iOS appAuth, profileSupabase JWTFastAPI on RenderEmbedding modelFAISS per sessionSupabasePostgres + pgvectorAuth, Edge FunctionGeminiCloud TTSGenerated contentWebhooksRevenueCat

The Flutter iOS app calls a FastAPI service on Render with the user’s Supabase JWT, and uses Supabase directly for auth and a few profile reads and writes. The service stores vocabulary, review state and embeddings in Supabase (Postgres with pgvector), runs the embedding model in-process, builds a small FAISS index per review session, and calls Gemini and Google Cloud TTS for generated content. RevenueCat webhooks land in a Supabase Edge Function, and an admin dashboard on Vercel drives the news pipeline.

Key technical decisions

Keep embeddings in Postgres, search small sets in memory

Embeddings live in pgvector columns next to the rows they describe, so they share the same row-level security, backups and migrations, and there is no second datastore to keep in sync. A dedicated vector database would have added a service to run and a sync path to get wrong, for little gain at this scale.

The two searches have different shapes. Recommendations search a large shared candidate pool, so that table has an HNSW index. Session building only compares one user’s due words (at most 1,000), so the backend loads them and builds an exact FAISS IndexFlatIP over normalized vectors in memory: exact cosine similarity, no index to maintain. I chose multilingual-e5-small because the input mixes English and Vietnamese and it runs on CPU.

Generate dictionary entries once and share them

A Gemini call per lookup would be slow and expensive, so each dictionary entry is generated once and stored in a shared table that every user reads. Generation is limited to whitelisted English words, rate-limited per user, and locked per word so concurrent lookups don’t trigger duplicate calls.

Stories are the exception. They depend on the exact five-word combination and the learner’s level, so caching them would rarely hit. They are generated fresh and protected by a daily quota instead.

Schedule reviews with an FSRS-based model

I chose FSRS over SM-2-style schedulers because it models each card’s stability and difficulty explicitly and predicts when recall drops below a target retention (0.9 here). The scheduler runs server-side in Python with the 21 FSRS-6 default weights, so every client gets the same schedule.

Five review steps have to collapse into one rating, so scoring is weighted by difficulty: multiple choice is worth 1 point, cloze 2 and spelling 3, normalized to a 6-point scale and mapped to Again, Hard, Good or Easy.

Treat user words as untrusted input to the model

Users can save any word and meaning, and those strings go straight into story prompts, so they are treated as untrusted. Before the call, words pass a profanity filter. In the prompt, they sit between explicit delimiters and the system prompt tells the model to treat that block as data and validate each word, reporting the ones it refuses. After the call, the output must match a JSON response schema, and refusal sentences are stripped before anything reaches the user.

Fail soft when Gemini is unavailable

Story quota is claimed atomically in the database before calling Gemini and released if the call fails, so users are never charged for an error. Calls go through a concurrency limit with retries and fail over to a backup model. If a story still can’t be generated, the session skips the Story and Cloze steps and gives full Cloze credit, so the review still completes and the schedule stays fair.

Results

Memorilla has about 150 registered users, who complete about 2,000 reviews a week. Generating a dictionary entry takes about 1.5 s at p50.

What I learned

  • Use a reference implementation for the scheduler. My hand-written FSRS mixes formulas from FSRS-4 to FSRS-6 and every user shares the default weights. Next step is a reference library plus per-user parameter fitting from review logs.
  • Grade on the server, not the client. Review results are reported by the app, so the next step is to bind each submission to a server-issued session.
  • Add observability before you need it. Without metrics, questions about cost and latency took far longer to answer than they should have.