System overview
teretere.ee is an Estonian learning product. Its main learning experience is a React app; a learner works through a structured course, guided practice, review, vocabulary, exam work, and optional deeper resources.
Browser ├─ React learning application (components/) ├─ Browser learner state (lib/store.ts) └─ Hash routes (#lesson/…, #resources/…) Next.js / Vinext application ├─ Pages and API routes (app/) ├─ Domain modules (src/contexts/) ├─ Product content (lib/ and learning catalogue) └─ Server integrations (lib/server/) Services ├─ PostgreSQL via Drizzle migrations ├─ Stripe billing ├─ Object storage for media/recordings └─ Email and Google sign-in when configured
The important rule is separation of concerns: components/ renders an experience,src/contexts/ holds business rules, lib/ holds shared product data and adapters, and app/api/ is the server boundary.
Project structure
This is the working map of the repository. Read from the top down: application entry points first, then interface code, then product rules, then shared adapters and operational tooling.
teretere.ee/ ├── app/ Next pages, layouts, API routes, and technical reference │ ├── api/ HTTP boundary: validate → authorise → call a context → respond │ ├── admin/ Editorial, campaign, and pilot staff pages │ └── technical/ This documentation and generated source inventory ├── components/ React interface, grouped by product area │ ├── app/ Application shell and hash routing │ ├── course/ Course map, lesson view, and lesson navigation │ ├── learning/ Guide, resources, labs, and learning interactions │ ├── assessment/ Diagnostic and exam UI │ ├── platform/ Shared product UI: auth, paywall, navigation, sync │ └── technical/ Documentation-only interactive components ├── src/contexts/ Product capabilities and business rules │ ├── learning-catalogue/ Lessons, exercises, words, and practice contracts │ ├── learner-progress/ Attempts, reviews, streaks, plans, and cloud sync │ ├── identity-access/ Accounts, sessions, roles, verification, and security │ ├── entitlements/ + billing/ Premium policy, Stripe checkout, portal, and webhooks │ ├── assessment/ + exam-practice/ Scoring, diagnostics, and mock exams │ └── teacher-classroom/, content-management/, speech-practice/, … ├── lib/ Shared content, client helpers, models, and server adapters │ └── server/ Database, Stripe, email, authentication, and other server-only code ├── db/ + drizzle/ Database schema/repositories and ordered migrations ├── public/ Browser-served static files ├── scripts/ Repeatable operational, content, seed, and documentation tasks ├── tests/ + test/ Unit, integration, architecture, and end-to-end coverage ├── worker/ Worker-specific runtime code ├── docs/ Longer written project documents ├── books/ Source learning-material assets └── package.json Commands, dependencies, build, test, and lint scripts
Generated or local-only folders such as .next/, dist/, build/,test-results/, and .wrangler/ are not source-of-truth application code.
Dependency direction
components/ ──may use──> src/contexts/<domain>/public.ts app/api/ ──may use──> src/contexts/<domain>/public.ts or server entry contexts/ ──may use──> their own domain/application/infrastructure layers lib/server/ ──may use──> provider SDKs and database adapters Do not reverse these arrows: a domain rule must not import a React component, and a component must not reach around a context straight into a server provider.
Feature guides
The overview explains shared architecture. Use a feature guide when you need to understand or change one capability without navigating the whole system. Each guide owns its detailed diagrams, contracts, recipes, and source entry points.
Start here
If you are new to the project, do not begin in a random component. Start by identifying the kind of change you need. The folders describe the boundary you should work in.
- I need to change what a learner sees.
- Start in
components/. Components compose the interface; they should call an existing domain function rather than recreate a business rule. - I need to change a product rule.
- Start in the relevant
src/contexts/<area>/domainorapplicationmodule. This is where behaviour should be testable without a browser. - I need a server endpoint.
- Start in
app/api/. Keep the route small: parse/authorize the request, call the owning context, return a response. - I need to change durable data.
- Inspect the existing server repository and add an ordered migration in
drizzle/. Never make a schema change only in application code. - I need to change course material.
- Start at the learning catalogue public entry point, then find the owning content file in
lib/or the catalogue infrastructure.
A useful reading order is: app/page.tsx → components/app/app-shell.tsx →components/app/app-route.ts → the screen component → the context it imports. That follows an actual learner route from entry point to business rule.
System maps
How the main parts connect
Learner interface
components/ ──renders──> domain public APIs ──uses──> domain/application rules
│ │ │
│ └──> infrastructure <──────┘
│ (browser, PostgreSQL, Stripe, R2, speech)
│
└──> app-shell route selection ──> screen component ──> progress action
app/api/ is the server boundary:
request → validate → authenticate/authorize → application service → responseClass and domain diagram
┌──────────────────┐ reads / updates ┌──────────────────────┐
│ React components │ ──────────────────────────> │ Browser learner store │
│ LessonView │ │ State + actions │
│ PracticeHub │ <────────────────────────── │ useSyncExternalStore │
│ App shell │ state snapshot └──────────┬───────────┘
└────────┬─────────┘ │
│ uses public APIs │ persists / syncs
v v
┌─────────────────────────────────────────────────────────────────────────┐
│ Domain contexts (src/contexts) │
│ LearningCatalogue LearnerProgress Entitlements IdentityAccess │
│ Billing ExamPractice SpeechPractice TeacherClassroom │
└─────────┬───────────────────────┬─────────────────────────────┬─────────┘
│ application rules │ server contracts │ adapters
v v v
┌──────────────────┐ ┌────────────────────┐ ┌─────────────────────┐
│ Static catalogue │ │ Next API handlers │ │ Infrastructure │
│ lessons, words, │ │ validate + auth + │ ───> │ PostgreSQL / Stripe │
│ exercises, media │ │ delegate + respond │ │ R2 / speech service │
└──────────────────┘ └────────────────────┘ └─────────────────────┘The arrows show dependency direction. A component can depend on a public context API; a domain rule must not depend on a React component. Infrastructure fulfils a domain contract, rather than defining the product rule itself.
Sequence diagram: complete an activity and sync progress
Learner Lesson component Browser store Progress sync /api/progress PostgreSQL │ │ │ │ │ │ │ answer activity │ │ │ │ │ ├───────────────────>│ │ │ │ │ │ │ recordAttempt() │ │ │ │ │ ├───────────────────>│ │ │ │ │ │ │ validate + save │ │ │ │<───────────────────┼────────────────────┤ render new state │ │ │ │ │ ├─────────────────>│ compare snapshot │ │ │ │ │ ├─────────────────>│ authenticate │ │ │ │ │ PUT snapshot ├────────────────>│ │ │ │ │ │ save/reconcile │ │ │ │ │<─────────────────┼─────────────────┤ │ │ │<─────────────────┤ authoritative snapshot / conflict │<───────────────────┼────────────────────┤ final learner state │ │
Notice that the browser store updates before the network request. This keeps the lesson responsive and protects work during temporary network loss; synchronisation handles the remote result afterwards.
Ownership map
components/ What the learner sees and does. app/ Next pages and HTTP route handlers. src/contexts/ Product rules grouped by business capability. lib/ Shared product content, client helpers, and server adapters. drizzle/ Ordered database changes. tests/ Behaviour contracts and regression coverage. scripts/ Content, migration, and operational tooling.
When you are unsure where code belongs, follow this rule: interface concerns go in a component; a reusable product decision goes in a context; provider/database/browser details go in infrastructure; and request parsing belongs at an API route boundary.
Runtime and delivery
- Framework
- Next.js 16 with React 19 and TypeScript.
- Development and build
vinext, Vite and Wrangler provide the local and deployment runtime.- Styling
- Global design tokens and shared styles live in
app/globals.css; a few complex views use CSS modules beside their component. - Icons
- Lucide React provides interface icons; there is no separate icon pipeline.
- Deployment model
- The app is prepared for a Cloudflare-compatible worker runtime. Server-only integrations are kept behind
lib/server/and API routes.
Routing and UI
The learner application at / uses hash routing so navigation works within the learning shell.components/app/app-route.ts parses the hash; components/app/app-shell.tsxchooses the screen and owns persistent navigation, session refresh, paywall checks, and route analytics.
#today— daily plan and resume point.#learnand#week/:week— course map and week overview.#lesson/:lessonId/:section— one focused lesson stage.#guide/:lessonIdand#resources/:lessonId— explanation and optional depth.#practice,#exam, and#progress— independent learning modes.
Normal Next routes are used where a page needs a distinct server boundary: legal pages, teacher pages, admin pages, pilot links, authentication callbacks, and this technical reference. Route scroll restoration is handled by components/platform/use-route-scroll-restoration.ts.
How a learner action works
Opening a lesson
- A link changes the hash, for example
#lesson/lesson-10/listen. subscribeRoutenoticeshashchange;parseAppRouteturns the string into a screen, lesson ID, section, and query parameters.Appinapp-shell.tsxchoosesLessonViewand passes the route details in.LessonViewreads the lesson from the learning catalogue, checks the learner’s entitlement, and renders only the requested stage.- When an activity is completed, a learner-progress action updates the browser state, schedules review where appropriate, and makes the new state available to progress sync.
Saving progress to the cloud
- A UI action calls a store action such as
recordAttempt,markActivity, orupdateState. lib/store.tsvalidates the full snapshot and writes it to browser storage first, so the learner does not lose work if the network is unavailable.ProgressCloudSyncand learner-progress sync code compare the local snapshot with the authenticated account’s remote snapshot./api/progressauthenticates the request, validates the payload schema, and calls the PostgreSQL repository.- The server returns the authoritative result; the client reconciles rather than silently discarding newer work.
Opening a Premium feature
- The route or component asks the entitlement context whether the lesson or feature is available for the current plan.
- If unavailable, the user sees a paywall with the correct return route; hiding a link alone is never the access control.
- Checkout is created by the billing route. Stripe sends a signed webhook; idempotent event handling updates the account plan once.
- The client refreshes its session and entitlement-dependent UI updates from the server-confirmed plan.
Domain modules
The codebase groups product rules into bounded contexts under src/contexts/. Most contexts use a small layered shape: domain for pure rules and types, applicationfor use cases, infrastructure for storage or third parties, and presentationfor HTTP-facing schemas or helpers.
- learning-catalogue
- Lessons, vocabulary, phrases, practice activities, textbook data, content checks.
- learner-progress
- Attempts, study plans, streaks, review scheduling, positions, drafts, and progress snapshots.
- identity-access
- Registration, sessions, password reset, email verification, Google sign-in, origin checks, rate limits.
- entitlements and billing
- Free versus Premium access rules, Stripe checkout, portal and webhook processing.
- exam-practice and assessment
- Mock papers, A2/B1 blueprints, diagnostics, scoring and coverage checks.
- teacher-classroom and content-management
- Classes, assignments, submissions, editorial updates and publishing controls.
- coaching, speech-practice, product-insights, access-campaigns
- Supportive learning features, recordings, analytics, pilots and campaigns.
Learning content and lesson flow
The catalogue is the source of truth for what is taught. Its public entry point issrc/contexts/learning-catalogue/public.ts. It exposes lessons, phrase and vocabulary data, reading/listening/writing/speaking tasks, textbook references, question exercises, diagnostics and mock papers.
- Course overview: ordered teaching sequence from
lib/course-sequence.ts. - Focused lesson:
components/course/course-lesson.tsxrenders one section at a time: Learn, Practice, Guide, Words or Resources. - Guidance:
lib/lesson-guide-content.tsprovides plain-language explanations and examples for the lesson topic. - Practice: lesson practice contracts select exercises using the learner’s current context.
- Review: progress rules schedule material to come back when it is useful, rather than treating every activity as a linear checklist.
- Optional depth: B1 learners can use extra enrichment; reference and dictionary tools remain available to every target.
Product content is primarily in lib/, the legacy catalogue adapter, and generated JSON assets. Images are public assets; audio is supplied through the speech API or configured speech service.
Learner state, browser storage and sync
lib/store.ts is the client-side state boundary. It uses React’suseSyncExternalStore, validates saved state with Zod, writes to browser storage underminu-eesti-v1, and listens for changes from another browser tab.
- Persistent: local browser storage is available and saves the learner state.
- Session: browser storage failed, so the current session continues without a durable local save.
- Recovery: saved data could not be validated; the app starts safely and offers export/restore paths.
- Cloud progress: authenticated users can synchronise progress through
/api/progress; conflict handling belongs to learner-progress.
Do not mutate the state object directly. Use actions such as recordAttempt, markActivity, or updateState, which keep validation, storage and subscribers consistent.
API surface
All HTTP endpoints are Next route handlers under app/api/. The main groups are:
- Authentication
/api/auth/*handles session, signup, sign-in/out, verification, reset and roles.- Learning progress
/api/progresssaves and restores learner snapshots;/api/eventsreceives product events.- Content and media
/api/content/*reads published content and media;/api/admin/content/*controls editorial changes.- Billing
/api/billing/*creates checkout, confirms payment, opens the customer portal and receives Stripe webhooks.- Teaching
/api/classes/*,/api/teacher/*, and/api/assignments/*power classroom and submission workflows.- Pilot and campaigns
/api/pilot/*and/api/admin/campaigns/*support invitations, feedback, reminders and reporting.
Request parsing is intentionally schema-based. Add or change a public request contract beside the owning context; keep route handlers thin and leave product decisions in application/domain code.
Core API contracts
These are the paths a junior developer is most likely to touch. Each contract states who can call it, what it expects, what it returns, and what the caller must handle. The source reference contains the related feature guides list the owning code and tests for the remaining endpoints.
POST /api/auth/signinStart a password session
- Caller
- Unauthenticated visitor signing in. Authentication must be configured.
- Input
- JSON validated by
signInRequestSchema(email and password). - Success
200with{ session }and aSet-Cookiesession cookie.- Expected failures
400invalid input,401bad credentials,403unverified account,429rate limit,500server failure.- Important rule
- Do not reveal whether an email exists. The handler intentionally uses the same credential error wording.
GET /api/progress and PUT /api/progressRead and save a cloud learner snapshot
- Caller
- Authenticated learner only. The response is private and must not be cached publicly.
- GET result
{ snapshot }, loaded for the signed-in account;401when there is no session.- PUT input
- A JSON snapshot accepted by
saveProgressRequestSchema, up to 2 MB, from a trusted mutation origin. - PUT result
{ snapshot }for a saved version.409returns the newer server snapshot when another device changed progress first.- Important rule
- Never overwrite a conflict blindly. Reconcile the returned snapshot with local work through learner-progress sync.
POST /api/billing/checkoutCreate a Stripe Checkout session
- Caller
- Authenticated learner from a trusted mutation origin.
- Input
- Optional
returnToandbillingPeriod(monthlyorthree_months). - Success
{ url }; the client sends the learner to Stripe rather than handling card data itself.- Failures
401unauthenticated;500unavailable billing provider or checkout failure.- Important rule
- A returned checkout URL does not grant Premium. Access changes only after billing confirmation/webhook processing.
GET /api/content/lessonsLoad the published course outline
- Caller
- Course screens that need editorial overrides for lessons and weeks.
- Result
{ lessons, weeks }from published content only; drafts are deliberately excluded.- Failure behaviour
- Returns empty lesson/week arrays if the content store is unavailable, allowing the built-in catalogue to remain usable.
- Important rule
- Do not use this endpoint to preview drafts. Draft editing belongs to the authenticated admin/content path.
POST /api/audio/speechGenerate approved Estonian narration
- Caller
- A trusted application origin, subject to per-account and request-security limits.
- Input
- A narration request accepted by the speech-practice validation rules. Text is normalized and constrained to published learning material.
- Result
- Cached or synthesized audio response with long-lived cache headers for approved narration.
- Important rule
- This is not a general text-to-speech proxy. Validation and rate limits exist to prevent arbitrary synthesis and cost abuse.
Identity, access and billing
Authentication is cookie/session based through the identity-access context. Mutating authentication calls check a trusted origin and apply action-specific rate limits. The app can also offer email verification, password reset, and Google sign-in when their environment settings are configured.
Entitlements determine what a free learner may open. Premium unlocks the full course and Premium-marked activities; this is enforced in domain access checks, not only by hiding buttons. Stripe events are made idempotent before changing a plan, so retries do not grant access twice.
Database, migrations and files
PostgreSQL is the durable server store. Drizzle schema changes are represented by ordered SQL migrations indrizzle/; use npm run db:generate for a new generated migration andnpm run db:migrate to apply it in the intended environment.
The database holds identity, progress, billing events, content revisions, campaigns, classroom data and related operational records. Static learning images live in public/. Larger recording/media workflows use the object-storage integration and the signed URL helpers behind their route handlers.
Common changes
Add or change a learner screen
First decide whether it belongs in the hash app or needs a standalone Next route. For the hash app, add the screen name to components/app/app-route.ts, render it in app-shell.tsx, then add navigation only if learners should reach it directly. Add component coverage before styling the edge cases.
Add a lesson activity
Put the activity contract and selection rule in learning-catalogue, use the existing lesson section components to render it, and record completion through learner-progress. Do not place grading or review scheduling inside a presentational component; that produces inconsistent behaviour across the lesson, practice hub, and exam modes.
Add an API endpoint
Define the request/response schema with Zod, authenticate and authorize at the route boundary, call an application service or repository, and test invalid input and forbidden callers. Keep secrets and provider calls in server/infrastructure code—never in a client component.
Add a database field
Add a forward-only Drizzle migration, update the repository and schemas, add a migration/integration test, and make old rows safe through a default or nullable transition. Do not edit an already-applied migration.
Add a Premium rule
Put the rule in entitlements, enforce it wherever the resource is loaded or mutated, then use presentation code only to explain the lock. Add tests for free access, Premium access, and direct-route access.
Quality, tests and operations
- Type safety
npm run typecheckruns TypeScript without emitting files.- Linting
npm run lintruns ESLint across the application.- Unit and component tests
npm testruns Vitest tests intests/.- Browser tests
npm run test:e2eruns Playwright;npm run test:e2e:criticalfocuses on critical regressions.- Integration tests
npm run test:integration:postgrescovers PostgreSQL-backed flows.- Content checks
npm run content:auditchecks practice and curriculum expectations.
Before changing a feature, find its owning context and existing test first. Before changing a user-facing route, verify both the UI behavior and access behavior. Before changing a migration, validate it against the migration journal and a disposable database.