AI product · Education technology

Building an AI tutor that only teaches what the learner already knows

Lumen is a Spanish-learning product for complete beginners: a streaming AI tutor, lessons generated from each learner's plan, generative practice and spaced repetition. I built it alone, from the first prototype to a three-tier product running in production.

Context
Own product
Year
2026
My role
Sole engineer: product design, architecture, frontend, backend services, AI integration and deployment
Browser Public edge Private Next.js app UI · SRS · grading Gateway Express · JWT auth AI backend FastAPI · prompts · RAG LLM APIs Gemini · OpenAI Documents S3 Postgres pgvector · cache streamed replies · schema-validated output · content-hash cache scope clamp: output stays within lessons already covered
Simplified system view — for illustration

The product

Lumen is a Spanish-learning environment for absolute beginners. A learner builds a study plan, and each lesson is generated from it. Around the lessons sit a streaming AI tutor, generated reading articles with comprehension checks, five-exercise practice sessions, and vocabulary review scheduled by spaced repetition. Learners can also upload their own documents, and the tutor uses them as reference material.

The problem

A general-purpose AI model makes a poor tutor for a beginner. Asked a simple question, it answers with vocabulary and grammar the learner hasn’t met yet. For someone at the very start, that makes the answer useless.

So the product had to be built around one constraint: everything the AI produces stays within what the learner has already covered.

What made it complex

  • Keeping the AI within bounds. Every generated output, whether a tutor reply, an exercise or an article, has to stay inside the learner’s current scope. That includes answers that draw on uploaded documents written at a much higher level.
  • Structured output from a model. Lessons and exercises are data the interface renders, not free text. A malformed response has to be caught before it reaches the learner.
  • Streaming across service boundaries. The tutor streams its answer through two services to the browser. Neither service can buffer it.
  • Cost and latency. Generation is slow and billed per call, so the same content shouldn’t be generated twice.

My responsibility

I built everything myself: the product and interface design, the architecture, the frontend, the gateway and backend services, the AI integration, the data model and the deployment.

Approach and key decisions

  • Scope as part of every prompt. Each request builds its prompt from the learner’s current and earlier lessons. Retrieved document content is added as reference only, and the output rules still apply to it.
  • Schemas on both sides. The backend validates generated content against Pydantic schemas. The frontend validates it again with Zod before rendering it.
  • A gateway as the only public entry point. The browser only talks to an Express gateway. The gateway owns authentication (JWT with refresh-token rotation) and forwards requests to the FastAPI backend using an internal service token. The backend is never reachable from the browser.
  • Generate once, then cache. Lessons, plan imports and topic suggestions are cached in Postgres, keyed by a hash of their input. The tutor and practice sessions are generated fresh every time.
  • Retrieval over the learner’s own documents. Uploaded files are parsed, split into chunks and embedded, then retrieved with pgvector. This lets the tutor answer from the learner’s material and cite it.
  • Learning logic stays in the client. Answer grading, spaced-repetition scheduling and weak-spot detection are plain code with no AI involved. Repeated mistakes are passed to the tutor as context.

Constraints and trade-offs

  • Two languages, two services. Node handles auth and the edge. Python handles AI and data. The cost is a shared database whose tables are split between two migration tools, and that boundary has to be managed carefully.
  • Start cheap, keep a path to scale. The API runs with Docker Compose behind Nginx on a single EC2 instance. A fuller AWS setup (ECS, RDS, S3) is already written as Terraform, and moving to it is a deployment change. The application code stays the same.
  • Accuracy over breadth. A narrower tutor that stays within scope is more useful to a beginner than a broader one that doesn’t.

What changed

Lumen started as a single Next.js app with no database. It now runs in production at lumenedu.app as a three-tier product: a Next.js frontend on Vercel, and an API served from AWS with real accounts, a Postgres + pgvector data layer, document retrieval, and CI on GitHub Actions.

Technology

  • Next.js
  • React
  • TypeScript
  • Node.js / Express
  • Python / FastAPI
  • PostgreSQL + pgvector
  • Gemini API
  • OpenAI embeddings
  • LangChain
  • Docker
  • GitHub Actions
  • AWS
Next case study