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_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.
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 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.
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 runbun 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
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 enabledprogramming-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.