This document defines the intended MVP architecture for Mathinik.
It bridges:
PRD.md— product truthCONTENT-SCHEMA.md— curriculum/content truthSTATE-SCHEMA.md— learner-state truth
This file should be used later to produce IMPLEMENTATION-PLAN.md and then GitHub issues.
Mathinik’s architecture should optimize for:
- interaction-first math learning
- offline-first PWA behavior
- local-first learner state
- simple authoring through static JSON content packs
- clean separation between content, state, lesson flow, and UI mechanics
- small, deep modules with stable interfaces
The architecture should protect the MVP from drifting back into a generic quiz app.
The architecture must treat interactive math-building as a first-class concern.
This means:
- activity rendering cannot be a thin wrapper around generic quiz widgets
- core interaction mechanics must have dedicated modules
- support formats like multiple choice and numeric input must not dominate the lesson engine design
The app should be usable once installed or loaded, even without reliable internet.
This means:
- app shell must be cacheable
- content packs must be cacheable
- learner progress must be stored locally
- content updates should be additive and safe when connectivity returns
Mathinik has two distinct data domains:
- content: grades, skills, lessons, activities, hints, explanations, rewards metadata
- state: child profiles, progress, mastery, resume state, unlocks, rewards earned
These must remain separate so:
- content can be versioned and updated safely
- state can be persisted locally without mutating authored lesson data
- testing remains cleaner
Each major subsystem should do one job well and expose a small interface.
That keeps the MVP maintainable and makes later GitHub issues easier to map to module boundaries.
Mathinik MVP is a client-heavy offline-capable web app.
High-level shape:
- App boots into the PWA shell
- App loads cached or bundled content packs
- App loads local learner state from browser persistence
- Parent selects or creates a child profile
- Child enters the map/home flow
- Lesson engine runs activities from content pack definitions
- Interaction engine renders the activity mechanic
- Results update local state: mastery, rewards, unlocks, resume
- Parent summary derives from local learner state
- When online, content update service checks for newer pack versions
Own the outer application frame and global lifecycle.
- routing
- app boot/loading state
- global error boundaries
- layout shell
- PWA install/update prompts
- lesson-specific logic
- mastery calculation
- content parsing rules
Load, validate, and provide authored curriculum content.
- static JSON content packs
- content schema validation rules
- typed/validated content pack objects
- lookup APIs for grades, skills, lessons, activities
loadContentPack()getGrades()getSkill(skillId)getLesson(lessonId)getActivity(lessonId, activityId)getContentVersion()
This module should be the only place that knows the raw pack layout. The rest of the app should consume normalized lesson/activity objects.
Persist and retrieve learner state from local browser storage.
- state schema
- profile changes
- lesson results
- unlock/mastery updates
- active profile state
- profile list
- persisted progress and resume data
loadStateStore()saveStateStore(state)createProfile(input)setActiveProfile(profileId)updateLessonProgress(profileId, patch)updateSkillMastery(profileId, skillId, mastery)updateResume(profileId, resume)
This module should hide storage implementation details. It should be possible to swap LocalStorage/IndexedDB strategy later without rewriting the lesson engine.
Handle parent-facing setup and profile selection.
- create child profile flow
- choose starting grade/path
- optional placement check path
- profile switcher
- content repository for available grades/paths
- local state store for profile creation and updates
- active profile selected
- optional placement recommendation stored
This module should remain lightweight in MVP. It is not a full account system.
Run the learner-facing lesson flow.
- lesson session lifecycle
- current activity progression
- hint/retry/explanation flow
- completion decisions
- transition to follow-up representations when needed
- validated lesson definition from content repository
- current learner state
- session result summary
- per-activity outcomes
- completion signal
- resume state updates
startLesson(profileId, lessonId)resumeLesson(profileId, lessonId)submitActivityResult(result)advanceLesson()completeLesson()
The lesson engine should not directly own rendering. It should coordinate flow and learning rules, while interaction-specific UI lives elsewhere.
Render and evaluate learner interactions.
equation-builderobject-manipulation
multiple-choicenumeric-input
- activity component dispatch by type
- interaction-level validation
- forgiving drag/drop behavior
- touch-friendly snapping and correction
- mechanic-specific event interpretation
ActivityRendererEquationBuilderActivityObjectManipulationActivityMultipleChoiceActivityNumericInputActivity
This is a critical architectural boundary. The interaction engine should make it easy to add future interaction types without changing lesson progression rules.
Interpret activity outcomes and decide feedback behavior.
- correct/incorrect evaluation
- hint trigger rules
- explanation trigger rules
- guessing-resistance checks
- follow-up representation triggers
- activity definition
- interaction result
- attempt history for the current session
- feedback action
- continue/retry/follow-up decision
Keep this separate from the raw interaction components. The interaction component captures what the child did. This module interprets what that means educationally.
Update learner progress after lesson/activity outcomes.
- mastery value updates
- mastery status changes
- star calculations
- reward granting
- lesson completion state
- unlock decisions
- replay improvements
- lesson results
- prior profile state
- lesson unlock/reward metadata
- updated learner state patches
- completion summary
- newly unlocked content
- earned rewards
calculateActivityOutcome()calculateLessonCompletion()updateMastery()determineUnlocks()applyRewards()
This module should be deterministic and testable in isolation. It is one of the most important “deep modules” in the system.
Present child-facing progression.
- resume card
- world map / progression path
- next recommended lesson
- badges and simple progress visuals
- content repository for progression structure
- learner state for unlocks/completion/rewards
- navigation to lesson start/resume
This module should remain mostly presentation-oriented. It should not contain mastery or unlock rules itself.
Present parent-facing progress summaries.
- completed lessons view
- stars/rewards summary
- simple mastery summary
- recent activity/resume context
- learner state only
- derived summary model for UI
This should be a derived-read model over local state, not a separate analytics subsystem.
Handle caching, content version checks, and safe updates.
- service worker coordination
- app shell caching
- content pack caching
- update detection
- safe swap/update rules
- content pack version metadata
- online/offline status
- cached content availability
- update notifications
- refreshed content pack data
This module should protect learner continuity. Do not allow careless content updates to break in-progress lessons or saved resume state.
- App Shell initializes
- Offline and Update Module prepares cached assets/content
- Content Repository loads current content pack
- Local State Store loads saved learner state
- Profile and Onboarding Module determines active profile or prompts setup
- App routes to child home or onboarding
- Child selects resume or lesson from map
- Lesson Engine requests lesson definition from Content Repository
- Lesson Engine requests current learner state from Local State Store
- Interaction Engine renders current activity
- Child performs interaction
- Interaction Engine returns structured result
- Evaluation and Feedback Module decides next action
- Lesson Engine advances, retries, or shows explanation
- On completion, Mastery and Progression Engine computes updates
- Local State Store persists changes
- Map and Progress UI updates available progression
- Parent Summary can reflect derived changes immediately
- Offline and Update Module detects connectivity
- App checks content pack manifest/version
- If newer content exists, download and validate
- Swap active content pack only when safe
- Preserve local learner state and existing lesson references where possible
Never store authored lesson data inside learner state. Never mutate content pack definitions to represent child progress.
This separation is essential for:
- offline content updates
- clean testing
- future sync/migration options
The interaction component should report what happened. The evaluation module should decide what it means.
This allows:
- cleaner mechanic components
- reusable educational rules
- better testing of guessing-resistance and scaffolding flows
The map UI should display unlocks and progress. The mastery/progression engine should decide them.
Avoid hidden business logic in UI components.
These internal normalized models should exist even if the raw JSON shapes differ.
Suggested fields:
lessonIdskillIdgradetitlegoalactivityIds[]estimatedMinutesunlockRulerewardRule
Suggested fields:
activityIdlessonIdtypepromptdifficultyhintexplanationfollowUpsuccessRuleuiModel
Suggested fields:
profileIddisplayNamegradeStartactiveResumecompletedLessonIdsunlockedLessonIdsskillMasteryrewards
Core mechanics may feel fiddly or confusing for young children on phones.
- keep mechanics narrow in MVP
- centralize forgiving interaction behavior
- test touch interactions early
Trying to create a super-generic activity engine too early can make core interactions weak.
- treat the two core mechanics as first-class modules
- allow support mechanics, but do not let the architecture center around quiz widgets
Offline content updates could break saved progress or resume state.
- version content packs explicitly
- preserve stable IDs
- treat content and learner state as separate domains
A full CMS sneaks into the MVP and delays the learner experience.
- keep authoring file-based for MVP
- do not add browser-based content editing to the core architecture yet
This is not the final stack decision, but the architecture suggests:
- component-based frontend
- strong TypeScript support
- schema validation at load boundaries
- local persistence abstraction over browser storage
- service worker / PWA support
- clean test support for touch-heavy UI
A likely fit is a modern TypeScript web stack, but stack choice should be finalized separately or folded into the implementation plan.
This architecture is designed to decompose into issues by module boundary.
Good issue buckets later:
- app shell + PWA boot
- content repository + validation
- local state store + persistence
- profile onboarding flow
- lesson engine
- equation-builder interaction
- object-manipulation interaction
- evaluation/feedback rules
- mastery/progression engine
- map/progress UI
- parent summary UI
- offline update flow
This is why ARCHITECTURE.md should come before IMPLEMENTATION-PLAN.md.
If opencode or another agent is asked later to implement Mathinik:
- read
PRD.md - read
CONTENT-SCHEMA.md - read
STATE-SCHEMA.md - read
ARCHITECTURE.md - then derive or follow
IMPLEMENTATION-PLAN.md
The architecture should be interpreted as:
- interaction-first
- offline-first
- local-state-first
- narrow MVP mechanics
- clean module boundaries