apps/learn and runs locally on port 7812. It is the learner-facing companion to the main Tuturuuu web platform: students and parents use Learn for lessons, practice, assignments, reports, marks, XP, streaks, and hearts, while teachers and admins continue to manage education data in apps/web.
Ownership model
Learn does not render its own login portal. Its/login route first checks for an existing Learn app-session JWT. If the app session is already present, it redirects inside Learn to the requested next path, usually /dashboard. Otherwise it redirects to the platform login at apps/web with a returnUrl pointing back to /verify-token. Learn stores a host-only tuturuuu_app_session cookie after token verification, not a Learn-local Supabase Auth session.
The /dashboard entry route must only send users to /login when the Learn app-session is missing. If the app-session exists but the central bootstrap API returns no eligible education workspace, render the empty workspace state instead of redirecting back to login.
Social auth providers still run on apps/web because OAuth provider callbacks are registered for tuturuuu.com. After the platform login confirms the current account, apps/web generates a cross-app token for the learn target app and redirects back to /verify-token. Learn’s local POST /api/auth/verify-app-token route only completes the host-only cookie handoff; token validation is delegated back to the central web app so Learn does not depend on a Learn-local Supabase Auth project. The handoff stores both a Learn-local app-session cookie for satellite route guards and a Web-issued app-session cookie for rewritten Web API requests. Protected Learn routes require both cookies; if an older local-only cookie is present, Learn sends the user back through the platform handoff so the coordinated cookie pair is refreshed without manual cookie deletion.
Protected product data still belongs to the central platform. Learn calls apps/web APIs through @tuturuuu/internal-api, and the satellite app rewrites /api/v1/* to the central web app. Learn-local API routes must stay limited to the auth cookie handoff and logout surfaces that require the Learn host.
Learn-local logout clears the host-only app-session cookies and stale Supabase Auth cookies on learn.tuturuuu.*, then redirects browser form submissions to the central apps/web /logout?from=Learn continuation. JSON callers can still POST /api/auth/logout and receive { success: true }.
Learn also exposes a learner-focused AI Chat surface at /[wsId]/ai-chat. The UI stays inside apps/learn, while the chat stream uses the central apps/web /api/ai/* routes through the satellite rewrite so model routing, credits, logging, and chat persistence remain centralized.
Workspace access
Every Learn workspace API requires both:- the authenticated user has student access or an explicit active parent-student link; and
workspace_secrets.name = ENABLE_EDUCATIONis set to the canonical valuetruefor that workspace.
personal do not diverge between selection and final reads. Linked learners in workspace_user_linked_users count as eligible student access even when the platform user is not a direct workspace member, so course lists and learner dashboard bootstrap must agree on the same workspace set.
Parent links
Parent access is explicit and read-only in v1. The existing Tulearn schema adds:tulearn_parent_student_linksfor accepted parent to student links;tulearn_parent_invitesfor invite-based parent linking;- admin-managed parent-link APIs under
/api/v1/workspaces/[wsId]/tulearn/parent-links.
Gamification
Learn-owned gamification behavior is backed by the existing Tulearn tables, separate from teacher-owned course data:tulearn_gamification_eventsstores XP events with an idempotency key per workspace and learner.tulearn_learner_statestores hearts, max hearts, total XP, streaks, freezes, selected workspace, and UI preferences. Direct authenticated writes are limited by RLS to the learner’s own row in workspaces where that learner is a member; non-nullselected_workspace_idvalues must also point to a workspace the learner can access.
award_tulearn_xp and lose_tulearn_heart. Those functions lock the learner state row and keep event insertion, streak updates, XP totals, heart decrements, and refill timer resets atomic under concurrent practice or assignment submissions. Streak dates currently use UTC day boundaries until Learn stores learner or workspace time zones.
Courses, modules, quiz sets, flashcards, assignments, monthly reports, and marks continue to reuse existing education and user-group tables.
Code organization
Learn learner pages keep the publicapps/learn/src/components/learner-pages.tsx entrypoint as a thin barrel export. Route-level screens live under apps/learn/src/components/learner-pages/*, with shared page motion, loading, empty state, and section primitives in shared.tsx.
The home dashboard should be a real learner workspace, not a landing page. Keep daily plan, quest board, learner toolkit, course path, assignments, marks, reports, practice, and AI Chat entrypoints visible as responsive Neobrutalist panels. Secondary rails should stack below the main grid until there is enough width for readable cards, and color should use dynamic theme tokens rather than a single accent.
Keep Learn learner files under 400 LOC and individual components under 200 LOC. When a screen grows, split reusable cards, rows, panels, and route-specific helpers into adjacent modules before final verification instead of letting learner-pages.tsx or any route screen become a monolith again.
Language switching is handled inside Learn with next-intl, but locale prefixes are hidden from URLs. Legacy locale-prefixed paths such as /vi/dashboard are canonicalized back to /dashboard while preserving the selected locale in NEXT_LOCALE. Keep English and Vietnamese strings in apps/learn/messages/en.json and apps/learn/messages/vi.json, then run bun i18n:sort.
Learn metadata and auth return URLs must resolve to absolute HTTP(S) app URLs. Prefer LEARN_APP_URL or NEXT_PUBLIC_LEARN_APP_URL for the app origin. A valid absolute BASE_URL can be used as a fallback, but non-URL environment values such as development are ignored so local development falls back to https://learn.tuturuuu.localhost through Portless.
Do not configure Learn return URLs with the retired tulearn.tuturuuu.com origin. Production learner redirects should use https://learn.tuturuuu.com, and teacher-facing redirects should use https://teach.tuturuuu.com.
Verification
After changing Learn schema, API, or UI code, run:CI and deployment
Learn has dedicated Vercel workflows:.github/workflows/vercel-preview-learn.yaml.github/workflows/vercel-production-learn.yaml
tuturuuu.ts and use the shared ci-check.yml switchboard. They require environment-scoped Vercel credentials plus VERCEL_LEARN_PROJECT_ID; production Supabase values should live in the Vercel project environment rather than GitHub Actions.
Deployment credentials must stay environment-scoped in GitHub Actions. The preview job is bound to the vercel-preview-learn GitHub Environment, and the production job is bound to vercel-production-learn. Store VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_LEARN_PROJECT_ID in those environments instead of repository-wide or organization-wide secrets. Do not add TURBO_TOKEN, TURBO_TEAM, or production Supabase service keys to workflow-level env; Learn deploys should rely on Vercel project environment variables pulled by vercel pull.
Preview dispatch is manual-only. Run vercel-preview-learn.yaml from main, set preview_ref to the reviewed branch, tag, or SHA, and keep TRUSTED_PREVIEW_DEPLOY_ACTORS limited to maintainers approved to run secret-backed preview builds. Manual production dispatch is only valid from refs/heads/production.
apps/learn/vercel.json disables Vercel Git deployments and GitHub integration so preview and production deploys only happen through the CI workflows.