KidLearn

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.

01

At a glance

The stack, one row per layer, with the reason for each choice
LayerChoice and reason
WebNext.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.
ServerExpress 5, TypeScript (ESM)A small, explicit REST API. Every route that takes input checks it with Zod, and every route is documented in OpenAPI.
DataPostgreSQL 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.
Authbetter-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.
AIGemini for text and images, Google Cloud Text-to-SpeechDrafts lessons, stories and narration in English and Bangla, and a person approves every one.
MediaCloudinary, 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.
CIGitHub Actions, one job called gatesLint, build, type-check, tests and real-database tests on every pull request.
02

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.

Workspace dependency diagram: apps/web imports packages/ui, i18n and types; apps/server imports db and types; the planned apps/mobile will import i18n, tokens and types; ui imports tokens; i18n and db import types.appsapps/webNext.jsapps/mobileplanned · Expoapps/serverExpresspackagesuiweb onlyi18nen · bn stringsdbPrismatokensdesign valuestypesZod schemas and API contractsconfigshared tsconfig basesimportsplanned
What imports what. The packages also share tsconfig bases from packages/config.

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.

Read the full document (opens in a new tab)

03

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.

Request path diagram: the browser loads pages from Next.js on Vercel, then calls the API directly with its session cookie; Caddy passes the call to Express on EC2, which checks the session, then reaches PostgreSQL through Prisma.Browserparent or childNext.js on Vercelpages and scriptsAPI call + session cookieCaddy on EC2TLS, the only open portsExpresssession checked here: requireParentPrismaone shared clientPostgreSQLSupabase, planned
Pages come from Vercel; data comes straight from the API.

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.

Read the full document (opens in a new tab)

04

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.

Read the full document (opens in a new tab)

05

The server is the authority

Stars, coins, streaks, badges, screen time and lesson completion are all worked out on the server. The app reports what happened, such as a finished step or a heartbeat while a lesson is open, and the server records it and decides what it means. It enforces step order, rate-limits reports, and pays nothing for a lesson until the steps that prove it are on record.

Learning time is not kept as a running total. It is added up from the session events themselves, so there is no counter for a device to inflate.

Read the full document (opens in a new tab)

06

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.

Publishing lifecycle diagram: draft goes to in_review on submit; in_review goes to approved when a person approves, or to rejected; approved goes to published; rejected goes back to draft for rework.draftsubmitin_reviewa person approvesrejectapprovedrejectedreworkpublishpublishedseen by children
Only published content reaches a child.

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.

Read the full document (opens in a new tab)

07

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.
08

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.

Read the full document (opens in a new tab)

09

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 the full document (opens in a new tab)

10

Read further