Skip to main content
Learn lives in 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 manage education data in Teach.

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 Learn 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 a Learn-local app-session cookie for satellite route guards and shared session material used when a retained platform service must be called. If older session material cannot be refreshed, Learn sends the user back through the platform handoff so the coordinated cookies are renewed without manual deletion. Learn owns learner and parent-facing v1 API contracts, including bootstrap, courses, modules, tests, practice, assignments, reports, marks, parent links, and vocabulary support. These handlers live under apps/learn/src/app/api/v1. Next.js fallback rewrites send only unmatched /api/v1/* and /api/ai/* paths to Web; they do not proxy a path that Learn implements locally.

Programming

The Learn Programming workspace is available inside eligible education workspaces. Its default layout fills the available dashboard viewport: a compact challenge and language picker sits above a resizable problem pane, code editor, and test console. Narrow screens use vertically resizable panes. Monaco runs in the browser with syntax highlighting for supported languages, and local JavaScript and TypeScript diagnostics and definition lookup. Local editor checks are guidance; only the platform Judge determines the result. Students can run public cases and one optional custom input/expected-output pair without consuming hidden cases. Submitting runs the public and private cases. Public inputs, expected outputs, actual outputs, diagnostics, and timing are visible to the learner. Private cases disclose only pass/fail and an aggregate count; their input, output, errors, and individual timing stay private. The default Judge feature is off on a newly connected Devbox until an administrator enables it and the machine reports a verified sandbox with cached pinned images. An unavailable language remains editable for learning but cannot run there. Parents may read the challenge and a linked learner’s execution history but cannot run or submit code. Each test run and submission records the source, language, timestamp, verdicts, and public case wall times in private platform tables. History is scoped to the learner, workspace, and challenge and can restore earlier source into the editor. The displayed median and range use the same monotonic wall-clock measurement for each public case. They include container startup and, where needed, compilation, so repeated runs may differ; the UI does not claim a hardware-normalized benchmark or rank users by these numbers. Hidden-case timings are omitted from learner responses. packages/education-core contains reusable server-only education domain logic; it does not own HTTP traffic. Programming catalog clients are exposed through @tuturuuu/internal-api/programming; keep new catalog consumers on that scoped entry point instead of extending the legacy education client module. The Tulearn helpers in packages/internal-api/src/tulearn.ts select the Learn origin for learner contracts. Explicit exceptions such as platform profile updates keep the Web origin. 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 its unmatched /api/ai/* requests use the Web fallback 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_EDUCATION is set to the canonical value true for that workspace.
The bootstrap route only returns eligible education workspaces and linked students:
Workspace-scoped learner routes use the normalized workspace id so aliases like 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 access is explicit and read-only in v1. The existing Tulearn schema adds:
  • tulearn_parent_student_links for accepted parent to student links;
  • tulearn_parent_invites for invite-based parent linking;
  • admin-managed parent-link APIs under /api/v1/workspaces/[wsId]/tulearn/parent-links.
Parents can switch between linked students in Learn. Parent requests may read home, courses, assignments, reports, marks, and practice context for linked students, but mutation routes reject parent writes.

Gamification

Learn-owned gamification behavior is backed by the existing Tulearn tables, separate from teacher-owned course data:
  • tulearn_gamification_events stores XP events with an idempotency key per workspace and learner.
  • tulearn_learner_state stores 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-null selected_workspace_id values must also point to a workspace the learner can access.
XP awards and heart loss must go through the database RPCs 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.

Programming route and operations

Learn owns the learner-facing Programming workspace at /[wsId]/programming; /[wsId]/coding redirects there. It starts with curated array, search, and stack exercises. Students in a workspace with ENABLE_EDUCATION=true can submit solutions; parents with read-only learner access cannot. The audited imported catalog is shared across eligible education workspaces; workspace managers can additionally author private workspace catalogs. Python, JavaScript, TypeScript, C, C++, Java, Rust, Go, Ruby, and PHP use the same standard-input/standard-output exercise contract. The selected language only becomes runnable when a platform Judge runner reports a cached pinned image for it. The fixed Judge command budget is 48,000 base64url characters for the combined source, catalog cases, and optional custom case. Individual field limits are upper bounds, not independently reserved capacity. Author saves reject catalogs that cannot fit even minimal source; execution validates the complete combined payload and rejects oversized commands before enqueue. Learners may reduce source or custom input to fit the shared budget. This applies to both workspace and imported catalogs and preserves the existing runner contract. The Programming workspace uses a full-bleed shell at this route, with shared Select, Button, Textarea, Tooltip, Tabs, and Accordion controls. Public test cases start collapsed in both problem and console panels. The bottom console can be resized or collapsed from the editor toolbar; collapsed content is inert, and completing a newly queued execution expands the console to show its result. Compact run/submit icons retain localized accessible labels and tooltips. The Judge readiness warning uses foreground text on a muted surface in both color modes and is informational; it does not enable a runner or change security. Code uses the self-hosted regular JetBrains Mono v2.304 font, scoped to this workspace. Its unmodified webfont, full SIL OFL 1.1 license, copyright and source/hash notice are colocated in the Programming component’s fonts folder. There is no operating-system font installation or additional font package. Submissions are stored in private.learn_coding_submissions and queued atomically with a private Devbox lease and typed Judge run. The default is at most 20 submissions per learner per hour. Learn reads only that learner’s submission, displays aggregate pass counts and visible case outcomes, and does not award XP or affect hearts yet. Hidden cases remain server-owned. If no eligible Judge machine is ready, the editor remains available and submission is disabled. Workspace members consume platform capacity; they do not control the machines. Root administrators manage runner feature toggles and Judge resource limits in Infrastructure > Devboxes.

Code organization

Learn learner pages keep the public apps/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.

Programming problem catalog

Programming opens its authorized catalog at /[wsId]/programming and each problem at /[wsId]/programming/problems/[problemId]. The legacy /coding entry redirects only after learner access is checked. Catalog pages return at most 50 summaries and expose a UUID continuation cursor; a selected problem remains accessible even when it is outside the first catalog page. Workspace authors require workspace membership, manage_users, and enabled education. Author forms can create and revise workspace problems; imported global problems remain read-only. Up to nine cases can be stored per problem, reserving the existing runner’s tenth slot for a learner’s custom test. Revision conflicts preserve the author’s form so another author’s changes are not overwritten. Learners receive published problem data and visible cases; hidden cases remain behind the authorized server boundary. Editor code, language, custom input, and inspected result selection persist per tab in session storage scoped to actor, workspace, learner, and problem. When storage is unavailable, drafts use memory for the current tab. History and polling bind to the opaque problem UUID; only the three audited imported global problems may read never-bound legacy slug history. Deleting a problem preserves existing submission history through its permanent identity binding. These decisions apply to the Learn catalog, problem workspace, author forms, and shared Programming lab. Backend access, case capacity, pagination, history, and subject regressions live in packages/education-core/src/education/programming-*.test.ts and apps/web/src/lib/programming-api.test.ts. The disposable actual Next/auth/API regression is apps/web/e2e/programming-catalog.noauth.spec.ts; it exercises RPC author writes, stale revisions, hidden-case filtering, and workspace isolation. Learn actor/history regressions live in apps/learn/src/lib/programming/server.test.ts and apps/learn/src/lib/coding/programming-store.test.ts; scoped draft regressions live in apps/learn/src/lib/programming/drafts.test.ts. The mounted canonical harness in apps/learn/tests/programming-workspace/run-canonical.mjs uses synthetic Next navigation and server-action adapters and does not replace authenticated Next/API/database integration evidence.

Verification

After changing Learn routes, run focused non-build route or component tests locally. Run applicable typecheck, lint, tests, and the owning Learn build in CI for the exact candidate commit. Do not run bun check or a product build locally; their task graphs may compile workspace dependencies. For the owning Learn build, dispatch .github/workflows/vercel-preview-learn.yaml on trusted main with preview_ref set to the immutable candidate SHA. Verify the checked-out source SHA and successful build step separately from preview deployment. Schema changes require disposable full-schema replay and generated types before integration; message changes require bun i18n:sort. Cover parent read-only access, student-only writes, XP idempotency, assignments, marks, and reports when those behaviors change.

CI and deployment

Learn has dedicated Vercel workflows:
  • .github/workflows/vercel-preview-learn.yaml
  • .github/workflows/vercel-production-learn.yaml
Production uses the shared workflow switchboard. Preview runs only by trusted manual dispatch on main, then checks out the supplied preview_ref. 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. Production Supabase values remain in the Vercel project environment pulled by vercel pull. The repository-level TURBO_TOKEN and TURBO_TEAM variable are passed only to the wrapped vercel build step; never place them at workflow or job scope, and never expose the token to pull-request or Dependabot code. 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. The full-bleed shell measures its mobile navigation header, including safe-area padding and wrapped rows, and reserves bottom safe-area space inside the viewport. Disabled Run and Submit controls expose their tooltips to keyboard focus. Font loading failures retain fallback editor metrics without unhandled rejections. Source regression coverage is in apps/learn/tests/programming-workspace/review-regressions.node-test.mjs; real mobile and Next build checks remain separate verification gates.

Programming database acceptance

The enabled programming-database-contract.yaml pull request gate checks out the exact source head and replays all repository migrations in an owned disposable Supabase project. learn-programming-full-schema.sql requires strict pgTAP success for grants, workspace boundaries, hidden-case redaction, stale revisions, invalid case visibility, and complete transaction rollback. Actual public schema types are uploaded under that source SHA after scoped cleanup succeeds; they are not committed by the workflow. The job has read-only repository permissions and a 45-minute deadline. This gate validates the current catalog contract without activating the separate inert hosted typegen proposals. Run the finite parser and session regressions with node --test apps/database/scripts/programming-database-tap.test.mjs apps/database/tests/programming-disposable/session.test.mjs. Use the repository isolated lifecycle for local schema acceptance, never a shared or production database. A failed migration, TAP assertion, type generation, or owned-project cleanup blocks acceptance.

Hosted projects and meetings

Programming also supports hosted playgrounds and the shared Meet coding workbench. Project saves belong to personal Drive; normal access requires a paid personal account or an active Infrastructure benefit. See account benefits for the centralized grant model and the realtime runbook for implementation, egress controls and operations.

Personal To do

The To do sidebar destination (/{wsId}/todo) is the signed-in actor’s own work in this education-enabled workspace. It intentionally ignores parent learner selection: the actor’s verified workspace-user link is resolved on the server, and studentId/other subject query overrides are rejected. A parent without an own learner link has an empty personal list; existing parent learner views remain available elsewhere. Tutoring lists explicitly assigned, pending, unresolved sessions. Assignments, lessons and tests come only from the actor’s actual STUDENT course memberships in active, non-guest, published courses. Only approved assignments and published lessons/tests are visible. Completion comes from this actor’s assignment checks, module completion records and submitted test attempts. Completed entries are hidden on the current page by default; the count includes assigned completed entries, and the toggle plus pagination avoids presenting an empty page as an empty workspace. Sessions show their recorded local date/time and duration, including explicit longer tutoring exceptions. Reads use Learn-owned /api/v1/workspaces/{wsId}/tulearn/todo and the shared internal API transport. They are private/no-store, paginated by category, and cached by app, workspace and verified actor. Failures offer Retry and do not turn unavailable data into an empty-success response. These existing app-owned education routes are outside the platform Web-to-Rust/TanStack migration inventory. Regression evidence: packages/education-core/src/todo/service.test.ts, both app-owned todo/route.test.ts files, internal API education-todo.test.ts, and shared UI education-todo.test.tsx. Exact app builds and authenticated runtime verification remain required delivery evidence.