f5dbc4f1c3e4f3fbdacb83107b8088da.192.0.2.1 proxied + www CNAME apex proxied (placeholders; Worker handles all).duolingo.id/*, www.duolingo.id/*.duolingo.id in web/xmoj.com/worker-cf/src/whitelist.js if using kie z-image piped through xmoj.Directory: web/duolingo.id/{worker,web,seed,scripts}. See prd.md for visual decisions.
| Decision | Choice | Why |
|---|---|---|
| Render style | React SPA + Worker API | Rich lesson UX |
| Worker router | Plain fetch URL match (no Hono) |
Matches astaga.net / nujum.id |
| React router | React Router v7 | Already standard (berkas.org) |
| Styling | Tailwind + CSS vars | Easy theme swap |
| State | Zustand | Light |
| Auth | Google OIDC + WA OTP via japri.com | Reuse idmu.org / japri.com infra |
| Audio | TTS → R2 cache by SHA1(text+voice) | Free, cacheable |
| Image | kie.ai z-image via xmoj → R2 forever | $0.0043/img |
| Mascot | Custom Garuda SVG | Avoid Duo IP, env-swappable |
| Analytics | CF Web Analytics + events table |
No cookie banner |
See schema.sql. Core tables: scripts, characters, words, units, lessons, exercises, users, user_lessons, user_state, srs_items, league_cohorts, quests, friends, events.
Binding DB → duolingo-db. Create with wrangler d1 create duolingo-db.
See wrangler.toml. Bindings: DB (D1), CACHE (KV), MEDIA (R2), AI (CF Workers AI), ASSETS (Pages-style asset binding for built SPA). Secrets: KIE_API_KEY, OPENROUTER_API_KEY, JWT_SECRET, GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, WA_RELAY_TOKEN, CRON_SECRET.
| Path | Purpose |
|---|---|
GET /api/health |
smoke |
POST /api/auth/google |
OIDC code → JWT |
POST /api/auth/wa-otp/send |
WA OTP |
POST /api/auth/wa-otp/verify |
verify → JWT |
GET /api/me |
user + state |
GET /api/courses |
list scripts |
GET /api/courses/:script/path |
path nodes + progress |
GET /api/lessons/:id |
lesson + exercises |
POST /api/lessons/:id/submit |
grade attempt |
GET /api/leaderboard |
cohort |
GET /api/quests |
active quests |
POST /api/quests/:id/claim |
claim reward |
GET /api/shop / POST /api/shop/buy |
shop |
GET /api/practice |
SRS due |
POST /api/events |
analytics |
GET /api/tts?text= |
TTS R2 cache |
GET /img/:slug.webp |
kie z-image on demand |
* |
SPA fallback via ASSETS |
| EN | ID |
|---|---|
| Streak | Beruntun |
| XP | Poin |
| Hearts | Nyawa |
| Gems | Berlian |
| Path | Jalur |
| Unit | Bab |
| Section | Babak |
| Quests | Misi |
| Practice | Latihan |
| Stories | Cerita |
| Shop | Toko |
| Friends | Teman |
| Leagues | Perunggu, Perak, Emas, Safir, Rubi, Zamrud, Kecubung, Mutiara, Obsidian, Intan |
seed/02_lontara_chars.sql.scripts/gen_content.mjs (OpenRouter Gemini Flash Lite).pro/tts-service → R2 cache by SHA1.Each lesson = 6–10 exercises (~3 min). Types: translit, choose, tap-pair, listen, build-word. Each wrong = −1 nyawa. End-of-lesson: confetti + Garu cheer + XP summary.
cd web/duolingo.id
wrangler d1 create duolingo-db # → fill database_id in wrangler.toml
wrangler kv namespace create CACHE # → fill id
wrangler r2 bucket create duolingo-media
wrangler d1 execute duolingo-db --remote --file=schema.sql
for f in seed/*.sql; do wrangler d1 execute duolingo-db --remote --file=$f; sleep 5; done
cd web && npm install && npm run build && cd ..
wrangler secret put KIE_API_KEY
wrangler secret put OPENROUTER_API_KEY
wrangler secret put JWT_SECRET
wrangler secret put CRON_SECRET
wrangler deploy
Verify:
curl https://duolingo.id/api/health # → {ok:true}
curl -I https://duolingo.id/ # → 200, HTML
File ucok-duo once scaffold lands. Children mirror this plan's phases.
See prd.md §10.
https://duolingo.id/ serves SPA shell/api/health returns {ok:true}translit exerciseThis domain MUST operate within these constraints — no exceptions:
If the plan above describes any flow that violates these constraints, treat the plan as ASPIRATIONAL only and rework before building. The constraint trifecta wins.
Ask AI to research, improve, or generate content.
Try: "Research competitors for this niche"