Architecture
v0.2 plan in progress. See IMPLEMENTATION_PLAN.md for the admin portal + Supabase content catalog rollout (chapters, translations, RLS roles). The diagrams below describe v0.1 (current); the planned v0.2 layout is at the bottom.
Stack (v0.1)
| Layer | Technology |
|---|---|
| Client | Expo + TypeScript + Expo Router |
| Local DB | expo-sqlite |
| Business logic | @umikora/shared |
| Content | content/packs/official-v1.json |
Stack (v0.2 — planned)
| Layer | Technology |
|---|---|
| Admin portal | Next.js 16.x (App Router) + shadcn/ui + Tailwind |
| Admin hosting | Vercel (preview on staging, prod on main) |
| Content store | Supabase Postgres (modules → chapters → cards, pathways, translations) |
| Mobile client | Expo + TypeScript + Expo Router (unchanged) |
| Mobile cache | expo-sqlite mirror of Supabase content |
| Auth (admin) | Supabase Auth (magic link) + RLS roles |
| Auth (mobile) | Anonymous (anon key); RLS enforces is_active=true reads |
| Offline fallback | content/packs/official-v1.json seed (legacy modules only) |
Layer diagram
┌─────────────────────────────────────────────────┐
│ app/ (screens) + src/components/ │
│ AppContext · SocialMenuContext │
└────────────────────┬────────────────────────────┘
│ repositories / useApp()
┌────────────────────▼────────────────────────────┐
│ src/data/localRepository.ts │
│ ContentRepository · ProgressRepository · │
│ StatsRepository │
└────────┬───────────────────────────┬────────────┘
│ │
┌────────▼────────┐ ┌─────────▼──────────┐
│ database.ts │ │ @umikora/shared │
│ SQLite schema │ │ sm2 · radar · │
│ migrations │ │ types · constants │
└────────┬────────┘ └────────────────────┘
│
┌────────▼────────┐
│ official-v1 │
│ .json │
└─────────────────┘
Repository interfaces
Defined in apps/mobile/src/data/repositories.ts:
- ContentRepository — modules, pathways, cards (read-only catalog)
- ProgressRepository — profile, study list, due cards, review submit
- StatsRepository — radar, summary, subscription tier (
getSubscriptionTier/setSubscriptionTier), reset
localRepository.ts implements all three. v1.1 will add SupabaseRepository behind the same interfaces.
App bootstrap
AppProvidercallsinitializeDatabase()on mountdatabase.tsruns migrations and seeds from JSON if neededready === true→ screens read sync data via repositoriesrefresh()bumps tick → context recomputes profile/stats/radar
Review lifecycle
getDueCards()from progress repo- User answers →
checkAnswer()→ rating →submitReview() submitReviewupdates SRS state viasm2.ts, logs review, updates XP/streak- Radar/stats recomputed on next
refresh()or context read
Freemium & mock subscription (v0.1)
Study-list and daily-new-card limits are driven by user_profile.subscription_tier (SQLite SCHEMA_VERSION 3). Tier limits live in @umikora/shared (getTierLimits()); plan names and HKD pricing live in apps/mobile/src/mocks/subscription.ts.
| Tier | 顯示名稱 | Study items | Daily new cards |
|---|---|---|---|
explorer |
探索者 | 2 | 10 |
scholar |
學者 | 5 | 30 |
philosopher |
哲人 | unlimited | unlimited |
Enforced in localRepository.ts: canAddStudyItem(), getDueCards(). Users tab provides mock subscribe/upgrade UI (Alert confirm, no RevenueCat). Legacy is_pro=1 rows migrate to philosopher. v1.1 replaces mock with RevenueCat — see TODO.md.
Content model (v0.2 — planned)
Three-level hierarchy, with all human-readable text split into translation tables:
Module (學科 / 書 / 考試)
└── Chapter (篇章 / 單元)
└── Card (閃卡: MCQ / Cloze)
Pathway (學習路徑)
└── module_slugs[] (ordered list of module slugs)
└── metadata: difficulty, estimated_days, prerequisite_slugs, subject_tags
Translations (one row per [entity, locale])
├── module_translations (zh-TW required, en optional)
├── chapter_translations
├── card_translations
└── pathway_translations
Supported locales: zh-TW (required) and en (optional). Adding a locale = update CHECK constraints + UI.
Full SQL, RLS policies, and audit triggers: IMPLEMENTATION_PLAN.md §3–§4.
v0.2 layer diagram (planned)
┌──────────────────────────────────────────────────┐
│ Admin Portal (Next.js, apps/admin/) │
│ Server Actions use signed-in user JWT (RLS) │
└──────────────────────┬───────────────────────────┘
│ writes
▼
┌──────────────────────────────────────────────────┐
│ Supabase Postgres │
│ modules / chapters / cards / pathways │
│ + *_translations + content_revisions (audit) │
│ + featured_items / featured_events │
│ + Edge Functions: featured-feed, set-user-role │
└──────────────────────┬───────────────────────────┘
│ anon read (RLS: is_active=true)
▼
┌──────────────────────────────────────────────────┐
│ Mobile App (Expo) │
│ supabaseContentClient → SQLite mirror │
│ Reads resolve title/description by preferred │
│ locale (user_profile.preferred_locale) │
└──────────────────────────────────────────────────┘
Related docs
- Implementation plan (v0.2): IMPLEMENTATION_PLAN.md
- SRS formulas: srs.md
- Radar axes: stats.md
- Screen map: MOBILE.md
- Infra & envs: INFRA.md
- Local setup: SETUP.md
- Agent index: …/AGENTS.md