Technical reference

How teretere.ee works

This is the practical map of the product: what runs where, where its learning material lives, how a learner moves through it, and where to change each part safely.

Open the learning appStart reading

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>/domain or application module. 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 → response

Class 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.
  • #learn and #week/:week — course map and week overview.
  • #lesson/:lessonId/:section — one focused lesson stage.
  • #guide/:lessonId and #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

  1. A link changes the hash, for example #lesson/lesson-10/listen.
  2. subscribeRoute notices hashchange; parseAppRoute turns the string into a screen, lesson ID, section, and query parameters.
  3. App in app-shell.tsx chooses LessonView and passes the route details in.
  4. LessonView reads the lesson from the learning catalogue, checks the learner’s entitlement, and renders only the requested stage.
  5. 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

  1. A UI action calls a store action such as recordAttempt, markActivity, or updateState.
  2. lib/store.ts validates the full snapshot and writes it to browser storage first, so the learner does not lose work if the network is unavailable.
  3. ProgressCloudSync and learner-progress sync code compare the local snapshot with the authenticated account’s remote snapshot.
  4. /api/progress authenticates the request, validates the payload schema, and calls the PostgreSQL repository.
  5. The server returns the authoritative result; the client reconciles rather than silently discarding newer work.

Opening a Premium feature

  1. The route or component asks the entitlement context whether the lesson or feature is available for the current plan.
  2. If unavailable, the user sees a paywall with the correct return route; hiding a link alone is never the access control.
  3. Checkout is created by the billing route. Stripe sends a signed webhook; idempotent event handling updates the account plan once.
  4. 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.

  1. Course overview: ordered teaching sequence from lib/course-sequence.ts.
  2. Focused lesson: components/course/course-lesson.tsx renders one section at a time: Learn, Practice, Guide, Words or Resources.
  3. Guidance: lib/lesson-guide-content.ts provides plain-language explanations and examples for the lesson topic.
  4. Practice: lesson practice contracts select exercises using the learner’s current context.
  5. Review: progress rules schedule material to come back when it is useful, rather than treating every activity as a linear checklist.
  6. 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/progress saves and restores learner snapshots; /api/events receives 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
200 with { session } and a Set-Cookie session cookie.
Expected failures
400 invalid input, 401 bad credentials, 403 unverified account, 429 rate limit, 500 server 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; 401 when 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. 409 returns 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 returnTo and billingPeriod (monthly or three_months).
Success
{ url }; the client sends the learner to Stripe rather than handling card data itself.
Failures
401 unauthenticated; 500 unavailable 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 typecheck runs TypeScript without emitting files.
Linting
npm run lint runs ESLint across the application.
Unit and component tests
npm test runs Vitest tests in tests/.
Browser tests
npm run test:e2e runs Playwright; npm run test:e2e:critical focuses on critical regressions.
Integration tests
npm run test:integration:postgres covers PostgreSQL-backed flows.
Content checks
npm run content:audit checks 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.