For engineers and recruiters
How KidLearn is built
The architecture, the decisions that shape it, and how it is tested and shipped. Every claim here links to the document or code it comes from.
At a glance
| Layer | Choice and reason |
|---|---|
| Web | Next.js 16, React 19, Tailwind CSS v4App Router pages built phone-first. Design tokens live in CSS variables, so two themes share one set of components. |
| Server | Express 5, TypeScript (ESM)A small, explicit REST API. Every route that takes input checks it with Zod, and every route is documented in OpenAPI. |
| Data | PostgreSQL through Prisma 6Relational data, with JSONB where content needs room to change. Row-level security is on for every table, so Supabase's public Data API cannot reach around the server. Production is planned to run on Supabase. |
| Auth | better-auth: Google for parents, email and password for adminsParents never create a password, and nobody can sign up for an admin account. An admin session ends 12 hours after sign-in, however active it is. |
| AI | Gemini for text and images, Google Cloud Text-to-SpeechDrafts lessons, stories and narration in English and Bangla, and a person approves every one. |
| Media | Cloudinary, uploaded from the browserSigned direct uploads, so no file passes through the API. |
| Hosting (planned) | Vercel for the web app; one EC2 instance running Docker Compose behind Caddy for the APINo load balancer and no NAT gateway: each would cost more per month than the instance itself. |
| CI | GitHub Actions, one job called gatesLint, build, type-check, tests and real-database tests on every pull request. |
The monorepo
One pnpm workspace, built with Turborepo: two apps and six packages. Shared code lives in packages, and dependencies point one way: apps import packages, never the reverse.
packages/ui is web-only by decision. Its Radix primitives need a browser DOM, so the planned Expo app will share tokens, types and strings with the web app, but not components.
A request, end to end
Vercel serves the pages, but the browser calls the API itself; there is no proxy in between. The API sets an httpOnly session cookie on its own origin, and the browser sends it with every call.
Caddy handles TLS and is the only thing listening on the public ports. Express refuses any state-changing request from an origin other than the web app's. Guards such as requireParent and requireAdmin check the session before a handler runs, and requireConsent holds back child data until a parent has agreed. Prisma is the only way into the database.
Content is data
Activities (drag and drop, tracing, matching, puzzles) and quiz questions are stored as JSONB, each with a schemaVersion. One set of Zod schemas in packages/types checks them when an admin saves them, when the AI drafts them, and when the app reads them.
The web app ships generic engines that render whatever a payload describes, so a new lesson is data, not code, and goes live without a deploy. Payloads carry a version so that older ones can be migrated as they are read, which keeps every write strict.
Publishing and the human gate
Content moves through draft, in_review, approved or rejected, and published. Every query a child's screen makes asks for published content only.
The AI pipeline drafts text and pictures with Gemini and narration with Google Cloud Text-to-Speech. A generation job can only end as awaiting_review or failed. The only way to publish AI content is a reviewer's approve decision, and the server refuses to publish without one.
Safety by design
- No social features
- No chat, no messages and no public profiles. Nobody can contact a child.
- No way out of the child's screens
- The child's screens contain no external links, and a test fails if one appears, even inside a lesson title written in the CMS.
- Consent before profiles
- The server refuses to create a child profile or record progress until a parent has given consent.
- Full deletion
- Deleting an account erases everything about the family in one database transaction. There is no soft delete and no grace period.
Quality
Biome lints and formats the whole repository, and tsc type-checks every package in strict mode. Vitest runs in every package that has code to test: 3,103 tests in 221 files, counted on 22 October 2026.
Every successful response in a route test is checked against its published schema with assertContract, and a coverage test walks the live Express routers and fails if any endpoint is missing from the OpenAPI document. What a stub cannot prove, such as cascades, unique constraints and the status check on related rows, runs against a real Postgres database in *.db.test.ts suites.
On every pull request, one CI job, gates, runs lint, build, type-check, the tests with coverage, the real-database suites and the Docker image builds.
Shipping
The design is deliberately small: Vercel's free tier for the web app, and one Graviton EC2 instance in Mumbai running the production and development APIs side by side behind Caddy. Supabase is planned for the production database, and Cloudinary holds the media.
The estimated running cost is about $14.08 a month for both environments (from the deployment guide, read on 22 October 2026), most of it the instance and its IP address. Cost is a design constraint, which is why there is no load balancer and no NAT gateway.
Where it stands on 22 October 2026: none of this is provisioned yet. Images are built and deployed with a script; a GitHub Actions deploy pipeline is specified but not yet built.
Read further
- Requirements and architecture (opens in a new tab)
- Database design (opens in a new tab)
- Design system (opens in a new tab)
- User journeys and manual (opens in a new tab)
- Engineering standards (opens in a new tab)
- Deployment guide (opens in a new tab)
- Operations runbook (opens in a new tab)
- Mobile app plan (opens in a new tab)