Skip to main content
Teach lives in apps/teach and runs locally on port 7813. It is the teacher-facing operations app for Tuturuuu education workspaces. Teachers create and publish courses, enroll existing workspace users, author modules, save attendance, write posts and reports, and enter metrics through Teach-owned APIs. Unmatched /api/workspaces/* requests, including workspace invitations and workspace details, use the Web fallback with their paths and queries intact. Local Teach API handlers retain precedence. Regression coverage lives in apps/teach/src/next-config.test.ts. Every Teach workspace data page and server loader must verify Teach app-session membership and its applicable workspace permission before reading data; layout redirects alone are insufficient (regression coverage: apps/teach/src/lib/teach-page-membership.test.ts). Education page and statistic-card gates require full (MEMBER) membership and the workspace’s ENABLE_EDUCATION flag, matching the education API. Guests are denied even when their default permissions include ai_lab. Each page also requires its specific permission. The overview remains available to ai_lab members; statistic cards whose individual permissions are missing render nothing. Direct page requests enforce these rules without relying on a parent layout. Membership and flag lookup failures fail closed with a sanitized server error and reach the error boundary; missing membership or a disabled flag still returns not found. The overview resolves membership and the flag once, passes the resolved permissions to its cards, and each card enforces its own permission before reading its count. This result is reused only within that render. Teach and Tulearn share the framework-free flag parser. Coverage: teach-page-membership.test.ts, require-teach-workspace-member.test.ts, teach-overview-membership.test.ts (streamed renders and one gate resolution per request), and education/access.test.ts.

Ownership model

Teach owns teacher authoring, administration, and shared education v1 API contracts. Its handlers cover courses, modules, quiz sets, flashcards, enrollment, attendance, posts, reports, metrics, attempts, grading, and related teacher workflows under apps/teach/src/app/api/v1. Teach also implements selected education AI handlers locally. Local handlers run before the Next.js fallback rewrites. Only unmatched /api/* paths go to Web, which remains responsible for platform login, cross-app token issuance, and explicitly retained platform or central AI services. New Teach-owned flows should add typed helpers in @tuturuuu/internal-api and consume those helpers from Teach client components with TanStack Query. packages/education-core owns reusable server-only domain logic, not HTTP traffic. packages/internal-api/src/teach.ts and education.ts select the Teach origin for teacher and shared education contracts; explicit platform exceptions such as storage operations retain the Web origin. When a Teach route uses an admin client to write rows keyed by request-body IDs, the route must validate those body IDs against the URL workspace and course before writing. Workspace permission plus course existence is not enough, because the admin client bypasses RLS; score writes such as indicator_id and user_id must prove that the indicator belongs to the URL course/workspace and the user is an active course member in the same workspace. Reads keyed by a quiz set or course module ID follow the same rule: the quiz-set quizzes, quiz-set linked-modules, and module quiz-sets API routes, the quiz-set dashboard layout and pages, and flashcard module links first confirm through apps/teach/src/lib/quiz-module-access.ts that the set or module (via its course group) belongs to the route workspace, returning 404 (or notFound()) before reading any link table, and filter joined rows to that workspace. Coverage lives in quiz-module-access.test.ts, the colocated route.test.ts files for those routes and flashcards/route.test.ts, and the dashboard quiz-sets/[setId]/data.test.ts. Courses are stored as workspace_user_groups. The course-level Learn visibility switch is workspace_user_groups.is_course_published; module visibility remains workspace_course_modules.is_published. New courses default to unpublished drafts, non-guest, active, and empty. Teach can enroll existing workspace users into a course, but this self-serve pass does not create or invite new users. Teach-owned attendance uses the course schedule fields on workspace_user_groups: sessions, starting_date, and ending_date. Teachers can generate recurring class sessions inside Teach, and the attendance calendar should mark unscheduled days, scheduled unchecked days, partial days, complete days, late days, and absent days without linking back to the web user-group schedule page. Teach-local logout clears the host-only app-session cookies and stale Supabase Auth cookies on teach.tuturuuu.*, then redirects browser form submissions to the central apps/web /logout?from=Teach continuation. JSON callers can still POST /api/auth/logout and receive { success: true }. Teach does not render its own login portal. Its /login route first checks for an existing Teach app-session JWT. If the app session is already present, it redirects inside Teach 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. After the platform login confirms the account, apps/web generates a cross-app token for the teach target app and redirects back. Teach’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, and Teach does not create a Teach-local Supabase Auth session. The handoff stores a Teach-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, Teach sends the user back through the platform handoff so the coordinated cookies are renewed without manual deletion. The /dashboard entry route must only send users to /login when the Teach app-session is missing. If the app-session exists but the Teach bootstrap API returns no eligible education workspace, render an empty teacher state instead of redirecting back to login. Learn follows the same platform-login pattern: the learner app keeps /verify-token for cross-app session completion, but /login delegates account selection to apps/web. Teach and Learn both hide locale prefixes from public and protected URLs. Legacy locale-prefixed paths such as /vi/dashboard redirect to the unprefixed canonical path while preserving the selected locale in NEXT_LOCALE. Teach proxy locale detection must sanitize Accept-Language before it reaches intl-localematcher. Wildcard or malformed locale tokens can throw a RangeError in the proxy and surface as a broken /login or protected course page even when the route itself still exists. Teach metadata and auth return URLs must resolve to absolute HTTP(S) app URLs. Prefer TEACH_APP_URL or NEXT_PUBLIC_TEACH_APP_URL for the Teach origin and LEARN_APP_URL or NEXT_PUBLIC_LEARN_APP_URL for Learn handoffs. A valid absolute BASE_URL can be used as a fallback for Teach, but non-URL environment values such as development are ignored so local development falls back to https://teach.tuturuuu.localhost through Portless. Teach module quiz, quiz-set, and flashcard pages and module-layout counts verify the module belongs to the URL course and resolved workspace before reading linked content or private quiz answers; foreign modules return 404 (regression: apps/teach/src/lib/course-module-pages.test.ts).

Visual direction

All non-apps/web education apps use the shared Neobrutalist design language from apps/learn/DESIGN.md: heavy foreground borders, offset shadows, paper-like surfaces, compact responsive grids, and theme-adaptive dynamic accents. Teach uses this language for public teacher orientation surfaces and protected dashboards. Teach should read as a teacher operations dashboard, not a landing page. The protected surface should keep courses, module authoring, learner enrollment, schedule-aware attendance, posts, report previews, score metrics, settings, and learner-preview handoffs visible as first-class paths. Use multiple dynamic accent roles across these work loops and keep rails readable in light, dark, and system themes. Teach module generation now accepts optional teacher context in the AI modal. Teachers can add class level, learning goals, or other constraints before uploading source material; the Teach-owned course-generation route passes that context into the AI prompt alongside the document content. Report authoring should include a Teach-local preview before save. The preview should show learner identity, course context, score, report body, feedback, and a metric snapshot so teachers do not need to open apps/web report pages for normal course reporting. When a learner-facing check is useful, link intentionally to apps/learn course, assignment, report, or marks pages. Teach reads and mutates course groups and module data through its local API handlers. Those routes resolve the Teach app-session actor and verify workspace, course, and resource access before using admin-backed data clients.

Verification

After changing Teach routes, run focused route tests first. Finish Teach API or UI changes with the app typecheck, repository checks, and the owning app build:
Run focused route or component tests when login bridging, cross-app token handling, local API authorization, or fallback forwarding changes.

CI and deployment

Teach has dedicated Vercel workflows:
  • .github/workflows/vercel-preview-teach.yaml
  • .github/workflows/vercel-production-teach.yaml
Both workflows are registered in tuturuuu.ts and use the shared ci-check.yml switchboard. They require environment-scoped Vercel credentials plus VERCEL_TEACH_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-teach GitHub Environment, and the production job is bound to vercel-production-teach. Store VERCEL_TOKEN, VERCEL_ORG_ID, and VERCEL_TEACH_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-teach.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.

Personal To do

The To do sidebar destination (/{wsId}/todo) lists this signed-in actor’s pending, unresolved tutoring assignments and teaching preparation work. Tutoring is matched by the verified workspace-user’s explicit teacher_user_id, not by homeroom membership, so tutoring another class works correctly. The teaching categories require actual TEACHER memberships in this workspace’s active, non-guest courses. Assignment preparation shows this teacher’s own unapproved posts; lesson/test preparation shows unpublished content in the courses assigned to them. It does not label the workspace’s whole operations catalog as personal work. Publishing/completing items remains in the existing assigned course views. The same interface and actor cache boundary as Learn apply: category pagination, explicit error/retry states, inherited theme, and no parent-selected subject. Teach’s first-class /api/v1/workspaces/{wsId}/teach/todo uses verified app-session authentication and private/no-store responses with the shared education loader and internal API transport. These app-owned education routes do not change the live Web API or a Rust-owned migration route. See Learn personal To do for regression coverage and bounded source-versus-runtime evidence. Teach quiz-set and module detail reads require verified membership of the route workspace and ai_lab, matching the education API permission. Denied members and nonmembers receive 404 before content, counts, or private quiz answers are read. Each loader checks independently of its layout. Regression coverage: education-page-access.test.ts and the quiz-set/module data suites. Bulk quiz creation validates all supplied module and quiz-set destinations in the resolved Teach workspace before creating quizzes, private answers, or links. Missing or foreign destinations return 404. Existing quiz updates retain their workspace filter. The Learn start, attempt, save-answer, and submit routes require the selected course in the student’s assigned, published courses for the resolved workspace. Parent and guest subjects keep their existing read-only restrictions. Course access is checked before test, answer, attempt, or grading operations. Coverage lives in Teach’s quizzes/workspace-links.test.ts and Learn’s tests/[testId]/access.test.ts. Creating a quiz set with a moduleId links it to that course module only when the module’s course group belongs to the resolved Teach workspace. Row-level security on course_module_quiz_sets only proves the module is visible to the caller, so a member of two workspaces could otherwise attach a quiz set to the other workspace’s module. The route checks the module’s workspace with the service client before any write; a missing or foreign module returns 404, a failed lookup returns 500, and neither creates the quiz set or the link. The created quiz set must also carry the resolved workspace id before it is linked. The quiz-sets/[setId]/modules link and unlink routes already apply the same rule through requireQuizModuleAccess. Coverage lives in Teach’s quiz-sets/route.test.ts.