A web application that helps preschool teachers auto-generate age-appropriate lesson plans. A teacher picks an age group, a theme, and a date, and the system returns a structured plan with a learning objective, an activity, a rhyme, a worksheet idea, and a materials list. Generated lessons are saved to a personal dashboard, can be searched by theme, and can be exported as a PDF.
The generator uses Google Gemini as the primary path and falls back to a rule-based template engine so that lesson generation always succeeds, even with no API key, no network, or Gemini downtime.
- Auth — email + password registration and login for preschool teachers.
- Lesson generator — input
ageGroup,theme,lessonDate; output objective, activity, rhyme, worksheet idea, materials. - Dashboard — list saved lessons, search by theme, open details, delete.
- PDF export — download any saved lesson as a printable PDF.
- AI with safety net — Gemini primary, deterministic templates as a guaranteed fallback. End-to-end generation SLA is 10 seconds.
- Security baseline — JWT auth, bcrypt password hashing, helmet headers, CORS allow-list, rate limiting, zod input validation.
| Layer | Tech |
|---|---|
| Frontend | React 18, Vite 5, Tailwind CSS 3, Axios, React Router 6, TypeScript |
| Backend | Node 20, Express 4, TypeScript 5, Zod, Pino |
| Database | PostgreSQL 16 (Supabase-compatible), node-pg-migrate |
| AI | @google/generative-ai (Gemini 1.5 Flash) + rule-based templates |
| Auth | JWT (jsonwebtoken) + bcryptjs |
| pdfkit | |
| Tests | Vitest (unit + integration + perf + security smoke), Playwright (E2E + a11y via axe) |
| Tooling | ESLint, Prettier, Husky, lint-staged, GitHub Actions CI |
| Deploy | Vercel (frontend), Railway (backend + database) |
.
├── backend/ Express + TypeScript API
│ ├── src/
│ │ ├── routes/ auth, lessons, export, health
│ │ ├── services/ gemini, fallback templates, generator orchestrator, pdf
│ │ ├── schemas/ zod request/response schemas
│ │ ├── middleware/ auth, error handler
│ │ ├── lib/ db, logger, supabase, migrations runner
│ │ └── config/ env loading and validation
│ ├── migrations/ node-pg-migrate SQL migrations
│ └── tests/ vitest suites
├── frontend/ React + Vite SPA
│ ├── src/
│ │ ├── pages/ HomePage, LoginPage, RegisterPage, DashboardPage, LessonDetailPage
│ │ ├── components/ RequireAuth
│ │ ├── context/ AuthContext
│ │ ├── hooks/ useAuth, useLessons
│ │ └── lib/ api (axios client)
│ ├── e2e/ Playwright specs (happy path, auth guard, ownership, a11y)
│ └── tests/ Vitest + Testing Library
├── docker-compose.yml Local Postgres on port 5433
├── .github/workflows/ CI (lint + typecheck + test)
└── DEPLOYMENT.md Step-by-step deploy guide
The monorepo is an npm workspace. Install once at the root and both packages get their dependencies.
- Node.js 20 or newer (see
.nvmrc) - npm 10 or newer
- Docker (for the local Postgres), or a Supabase project
npm installThis installs the root dev dependencies (Prettier, Husky, lint-staged) and
the workspaces (backend, frontend).
docker compose up -dThis brings up Postgres on localhost:5433 with user lesson, password
lesson, database lesson_dev. To use a hosted Supabase project instead,
fill in DATABASE_URL and DATABASE_DIRECT_URL in backend/.env.
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.envEdit backend/.env to point DATABASE_URL at your database and set a strong
JWT_SECRET (32+ random characters). GEMINI_API_KEY is optional — leave it
empty to use the rule-based fallback only.
Get a Gemini key at https://aistudio.google.com/app/apikey.
npm run migrate:up --workspace backendThe backend also runs migrations automatically on production startup.
npm run dev:backend # http://localhost:4000
npm run dev:frontend # http://localhost:5173The frontend reads VITE_API_BASE from frontend/.env and defaults to
http://localhost:4000. Open http://localhost:5173, register a teacher
account, and generate a lesson.
Run from the repository root unless noted.
| Script | What it does |
|---|---|
npm run dev:backend |
Start the API with tsx watch (auto-reload) |
npm run dev:frontend |
Start the Vite dev server |
npm run build |
Build backend (tsc) and frontend (tsc + vite) |
npm run lint |
ESLint both workspaces, warnings are errors |
npm run test |
Run Vitest in both workspaces |
npm run format |
Prettier write across the repo |
npm run format:check |
Prettier check (CI mode) |
npm run typecheck |
tsc --noEmit per workspace |
npm run migrate:up |
Apply pending DB migrations (backend) |
npm run migrate:down |
Roll back the last migration (backend) |
All /api/lessons/* and /api/export/* routes require a Bearer JWT in the
Authorization header. Register or login to obtain a token.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
no | Create a teacher account. Returns JWT + user. |
| POST | /api/auth/login |
no | Authenticate, return JWT + user. |
| POST | /api/lessons/generate |
yes | Generate a lesson from ageGroup, theme, lessonDate. Persists and returns the saved plan. |
| GET | /api/lessons |
yes | List the caller's lessons (newest first). |
| GET | /api/lessons/:id |
yes | Fetch one lesson. 404 if it is not the caller's. |
| DELETE | /api/lessons/:id |
yes | Delete one lesson. 404 if it is not the caller's. |
| GET | /api/export/pdf/:id |
yes | Stream a PDF rendering of the lesson. |
| GET | /health |
no | Liveness probe. Returns version, uptime, Gemini status. |
{
"ageGroup": "3-4",
"theme": "Ocean Animals",
"lessonDate": "2026-06-08"
}{
"id": "uuid",
"ageGroup": "3-4",
"theme": "Ocean Animals",
"lessonDate": "2026-06-08",
"lessonContent": {
"objective": "...",
"activity": "...",
"rhyme": "...",
"worksheet": "...",
"materials": ["..."]
},
"source": "gemini",
"createdAt": "2026-06-07T12:34:56.000Z"
}source is "gemini" when generated by the API, "fallback" when generated
by the rule-based templates.
users
| Column | Type | Notes |
|---|---|---|
| id | uuid | primary key |
| name | text | |
| text | unique | |
| password | text | bcrypt hash |
| created_at | timestamptz | default now() |
lesson_plans
| Column | Type | Notes |
|---|---|---|
| id | uuid | primary key |
| user_id | uuid | foreign key → users.id |
| age_group | text | |
| theme | text | indexed for search |
| lesson_date | date | |
| lesson_content | jsonb | objective, activity, rhyme, worksheet, materials |
| source | text | gemini or fallback |
| created_at | timestamptz | default now(), indexed desc |
Migrations live in backend/migrations/ and are applied with
node-pg-migrate.
The generator is implemented in
backend/src/services/generator.orchestrator.ts:
- Try Gemini with
GEMINI_TIMEOUT_MS(default8000) and the modelGEMINI_MODEL(defaultgemini-1.5-flash). - If Gemini throws, times out, returns malformed JSON, or the env var is
empty, call the rule-based template engine
(
backend/src/services/fallback.templates.ts). - Persist the result with the actual
sourceso the UI can show which path produced the lesson.
Generation must complete within 10 seconds end-to-end. The fallback is deterministic and never blocks on a network call.
- Unit and integration tests live in
backend/tests/andfrontend/tests/and run with Vitest. Coverage is published fromvitest run --coverage. - End-to-end specs live in
frontend/e2e/and run with Playwright. Thea11yspec uses@axe-core/playwrightto assert no serious WCAG violations on the main flows. - A perf smoke test (
backend/tests/perf.smoke.test.ts) and a security smoke test (backend/tests/security.smoke.test.ts) guard the 10-second SLA and the basic auth / ownership / rate-limit posture.
npm test # all vitest suites
npm run test --workspace backend # backend only
npm run test --workspace frontend # frontend only
npx playwright test --config frontend/playwright.config.tsThe production target is:
- Frontend — Vercel, root directory
frontend, buildnpm run build, outputdist. - Backend & Database — Railway, root directory
backend, buildnpm install && npm run build, startnpm start. Migrations run automatically on startup.
The backend service is linked to a managed PostgreSQL database service on Railway, with env variables mapped accordingly. The frontend reads VITE_API_BASE at build time to communicate with the Railway API.
See DEPLOYMENT.md for the full step-by-step, including environment variables, smoke tests, and how to rotate the Gemini key.
- ESLint config:
backend/.eslintrc.cjs,frontend/.eslintrc.cjs. Both run with--max-warnings 0. - Prettier config:
.prettierrc.jsonat the root. - Husky
pre-commithook runslint-staged, which formats staged files with Prettier. - GitHub Actions runs lint, typecheck, and tests on every push and pull
request — see
.github/workflows/ci.yml.
Per the product brief, the following are intentionally deferred: parent portal, WhatsApp integration, admin dashboard, lesson sharing, analytics dashboard, multi-center management, and the mobile app.