← All work A-05 Case · /en/dev/cue

Cue

An English trainer for Russian-speaking learners: words and rules come from live conversation with an AI and come back as spaced-repetition cards — recognise, fill the gap, hear, pronounce, quiz, use in a sentence, dictation from YouTube.

Status
Closed testing
Role
Everything: backend, auth service, MCP server, mobile client
Stack
Python 3.13FastAPISQLAlchemy 2 asyncPostgreSQL 17RedisRabbitMQExpo / React NativeReact 19TypeScriptFSRSClaudeElevenLabsAzure Speech
W 1160
A-05390 × 844
Cue review session: a word shown in an example sentence about hikingCue review session: a word shown in an example sentence about hiking
Cue home screen: rule of the week, talk, review, candidatesCue home screen: rule of the week, talk, review, candidates
Cue chat with the AI: an explanation with examples and a card proposalCue chat with the AI: an explanation with examples and a card proposal
Web build of the same Expo code as the Android app.
01What it does 7 card facets

Words from conversation, back as cards

The learner talks to the AI — by text or voice, sometimes in role-play scenes. The model explains and proposes a card; the learner decides whether to keep it. From then on the item comes back in short sessions scheduled by FSRS, each time as a different facet: recognise the word in an example, type it into a gap, hear it, pronounce it (scored by Azure), pass a quiz, use it in a sentence (checked by the model), or write down a phrase from a YouTube clip.

Exercises and audio are generated ahead of time, to the learner's level and interests. On any card you can ask a side question ("By the way") or report a mistake in the exercise. Friends send each other cards and phrases. Words can also be added from Claude, ChatGPT or Cursor through the MCP server.

02Services 3 services · 5 processes · 1 compose3 services

How it is built

Server · docker compose Stores
Expo client · Android, web
MCP clients · Claude, ChatGPT, Cursor
Caddy
TLS
api · FastAPI
worker · asyncio + aio-pika
auth + bot · OAuth 2.1, ES256
mcp · MCP SDK
RabbitMQ
PostgreSQL 17
Redis
LLM · Claude, OpenAI-compatible
ElevenLabs · TTS, Scribe
Azure Speech · pronunciation
YouTube · yt-dlp
Google OAuth
Firebase · pushes
media/
token exchange
Legend
  • HTTP
  • external
  • requests
  • store
docker compose Stores External
Expo client · Android, web
MCP clients · Claude, ChatGPT, Cursor
Caddy · TLS
api · FastAPI
RabbitMQ
worker · asyncio + aio-pika
auth + bot · OAuth 2.1, ES256
mcp · MCP SDK
PostgreSQL 17
Redis
media/
LLM · Claude, OpenAI-compatible
ElevenLabs · TTS, Scribe
Azure Speech · pronunciation
YouTube · yt-dlp
Google OAuth
Firebase · pushes
token exchange
Legend
  • HTTP
  • external
  • requests
  • store
Three services in five processes, one docker compose; the MCP server never talks to the API with the client's token — it exchanges it for a short one.
03Numbers snapshot

In numbers

  • 957 tests
  • 44 tables
  • 83 HTTP endpoints
  • 118 LLM eval cases, run against the live model

Also: 7 card facets · 20 prompt templates · 108 commits in six weeks · 27k + 26k lines of Python / TypeScript.

04Engineering decisions 5 decisions

What was hard

Decision 01

Exercises are prepared for the next two sessions, not for the whole dictionary.

Everything shown repeatedly — exercises, audio — is generated in the background and waits in the database, so a card never waits for the model. But generating for the whole dictionary is wasteful, so only the window of the two upcoming sessions is prepared, up to three variants per card and exercise type, and the session plan is deterministic so the window and the queue agree.

Decision 02

Pronunciation: own chunking over Azure REST

Azure's short-audio REST endpoint recognises one phrase up to the first pause and silently drops the rest. Free speech is therefore cut server-side: 20 ms frames, a voice threshold from loudness percentiles over the noise floor, a chunk ends in the middle of a pause of 600 ms or more and never exceeds 30 s. The first version sent 6–14× more audio than was recorded — and Azure bills by the second. Scores map to an FSRS grade through a versioned threshold table; unrecognised speech returns 422 "say it again" and leaves nothing behind.

A-05.2record → score
Cue pronounce card with a microphone buttonCue pronounce card with a microphone button
Cue pronunciation result: accuracy, score and the recognised textCue pronunciation result: accuracy, score and the recognised text
The card before and after scoring.
Decision 03

The review log is never rewritten

review is append-only — UPDATE is forbidden by a database trigger, and so are all answer tables. When the model writes a faulty exercise (it happens: a quiz key with "gonna" missing "is"), the complaint is checked by a separate model call without the chat history, and the grade is fixed by a correction row, after which the card's state is recomputed from its log. This works because FSRS runs without interval fuzzing: replaying an unchanged log yields exactly the stored state.

A-05.3explained by the model
Cue cloze card checked: 'through' instead of 'thorough', with the model's explanationCue cloze card checked: 'through' instead of 'thorough', with the model's explanation
Cue 'use it' card: the corrected sentence and the explanationCue 'use it' card: the corrected sentence and the explanation
Every answer is an append-only row; a wrong exercise is fixed by a correction row, never by an UPDATE.
Decision 04

Auth as a separate service; MCP through token exchange

The backend knows no passwords and issues no tokens: it verifies the auth service's ES256 token via JWKS. Auth is a full OAuth 2.1 server — mandatory PKCE, audience selection, refresh-token rotation with family revocation, dynamic client registration, a consent page for third-party clients. The MCP server receives a token for its own audience and exchanges it for a short-lived API token (RFC 8693). Access to the app is a separate entity, granted by the admin with a button in a Telegram bot.

Decision 05

Evals, not eyeballing

Everything the model produced is stored with the prompt version, the model and a profile snapshot; a prompt changes only as a new template version. 118 eval cases run against the live model — each with its own run count and pass threshold, reported with a 95% interval, token use and cost; several models side by side. The first pass on a new model gave nine cases below threshold; eight templates got new versions and the full set was run three times.

05Screens 390 × 844
06Timeline 06.09 → 16.10.202606.09 → 16.10

Six weeks

  1. 06.09.2026First commit: README spec, backend skeleton with reviews and the queue, mobile client skeleton
  2. 07.09Audio, pronunciation scoring, CI/CD — first production deploy
  3. 15–17.09"By the way", separate auth service, MCP server, web version, YouTube index and dictation
  4. 03–09.10Friends, review sessions and the preparation window, onboarding interview
  5. 10–11.10"Reading room" design system, Google Play preparation, FCM pushes, LLM evals
  6. 12–16.10Long speech in chunks, session plan instead of due-date order, promo video, exercise reports with grade recomputation
07Integrations 11

What it talks to

  • Claude
  • OpenAI-compatible API
  • ElevenLabs TTS
  • ElevenLabs Scribe
  • Azure Speech
  • YouTube (yt-dlp)
  • Google OAuth
  • Telegram Bot API
  • Firebase Cloud Messaging
  • Google Play
  • GitHub Actions