* feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * feat(self-host): safe versioned releases + pull-based image pipeline Builds on the pull-based images compose (docker-compose.images.yaml) to make pulling pre-built images safe for self-hosters: - docker-publish: stable-releases + edge tag policy. Git tag vX.Y.Z publishes X.Y.Z/X.Y/X/latest; a push to main publishes only :edge (latest never moves on a routine merge). - Pre-publish smoke-test gate: each image is booted against ephemeral postgres+redis and healthchecked before any push (docker/smoke-test.sh). - CIO_VERSION pinning in .env.example; run-docker-full-stack.sh --images flag to pull instead of build. - Docs: SELF_HOST pull-from-images + versioning/upgrade/rollback sections, PUBLISHING_IMAGES tag policy + pnpm release flow, self-hosting.mdx quickstart. * feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * ci: run build-only on PRs, defer lint until repo-wide format * docs: point Railway template link at the new template URL * fix: address Greptile review + dashboard CI build env - docker-push.sh: only also-tag :latest for real semver releases, so VERSION=edge/main/dev can't clobber the stable latest pointer (P1). - run-docker-full-stack.sh: stop deriving OBJECT_STORAGE_PUBLIC_ENDPOINT from the dashboard origin (broke presigned URLs for videos/documents buckets); derive only the public media base URL and document the S3-API route. - ci.yml: provide PUBLIC_IS_SELFHOSTED at build time (dashboard reads it from SvelteKit's strict $env/static/public); bump cache actions v3 -> v4. - upload-openapi-spec.yml: bump cache actions v3 -> v4. - smoke-test.sh: dashboard gate now fails on 5xx instead of accepting any response. * ci: scope PR build gate to the api + dashboard closures Building every package surfaced pre-existing rot in apps that aren't part of this PR (storybook's missing @lucide/svelte import, course-app prettier debt). The gate's purpose is to verify the two Docker-image apps; the rest are built on PRs by their own Cloudflare Workers builds. Filter to @cio/api... and @cio/dashboard... so the gate covers exactly what it should. * fix(docker-push): make latest_arg always return 0 Use an `if` instead of `is_release_version && printf` so the helper returns 0 even when it emits no tag. A non-zero return inside the $(latest_arg ...) build argument is fragile under set -e (and would abort with inherit_errexit), which would break the edge/main builds the policy explicitly supports. * fix(docker): copy patches/ into jobs image so pnpm install resolves patchedDependencies The jobs Dockerfile ran `pnpm install` without copying patches/, so pnpm failed hashing package.json#pnpm.patchedDependencies (layerchart patch), breaking the self-host full-stack build. api/dashboard already copy patches/. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(docker): build jobs worker deps in topological order The jobs Dockerfile built workspace packages with a hand-written list that omitted transitive deps (e.g. @cio/question-types, consumed by @cio/utils), so `tsc` failed with TS2307 during the self-host build. Use the recursive `pnpm -r --filter @cio/jobs-worker... build` like the api/dashboard images, which builds the full dependency closure in the correct order. * refactor(docker): move jobs Dockerfile to docker/Dockerfile.jobs and align dep install Move apps/jobs/Dockerfile -> docker/Dockerfile.jobs so all three image recipes live together in docker/, and update every reference (docker-compose.yaml, docker-push.sh, docker-publish.yml, railway.jobs.json, self-hosting.mdx). Also align its dependency install with the api/dashboard images: replace the hand-maintained list of workspace package.json copies (which silently missed transitive deps like @cio/core / @cio/ai-assistant, leaving their node_modules uninstalled and breaking the build with TS2307) with the proven pnpm-fetch pattern: fetch the full lockfile into a cached store, COPY . ., then an offline filtered install of @cio/jobs-worker... This installs the complete dependency closure regardless of how the workspace graph grows. * refactor(docker): move docker-compose.yaml to repo root for Coolify compatibility Coolify's Docker Compose build pack forces Base Directory to the repo root and resolves build contexts from there, so the compose's `context: ..` (needed when the file lived in docker/) climbed above the repo and failed with `lstat /artifacts/docker: no such file or directory`. Move docker/docker-compose.yaml -> ./docker-compose.yaml and change the build contexts from `..` to `.` so the context is the repo root with no upward path. This works identically for run-docker-full-stack.sh, manual `docker compose up`, and Coolify. Dockerfiles stay referenced as docker/Dockerfile.*. Update the references in run-docker-full-stack.sh and the docs accordingly. The images compose (docker-compose.images.yaml) is unaffected (no build context) and stays in docker/. * Add self-hosting documentation for Coolify and Railway - Created new documentation for deploying ClassroomIO on Coolify using a pull-based Docker Compose stack. - Added comprehensive guide for self-hosting ClassroomIO with Docker, covering both pre-built images and building from source. - Introduced a new meta.json file to organize self-hosting documentation pages. - Developed a Railway deployment guide with environment variable configurations for API, dashboard, and jobs worker. - Updated self-hosting blog post to link directly to the new Railway guide. - Removed obsolete Railway configuration files (railway.api.json, railway.dashboard.json, railway.jobs.json). - Enhanced run-docker-full-stack.sh script to improve MinIO credential generation and service readiness checks. - Updated docker-compose files to enforce strong secret requirements for authentication. - Adjusted self-hosting references in the Docker documentation to include new guides. * fix(docker): point references at root-level docker-compose.images.yaml The pull-based compose file moved to the repo root (alongside docker-compose.yaml). Update IMAGES_COMPOSE_FILE in run-docker-full-stack.sh, the file's own header example, and the docs (docker.mdx curl URL, coolify.mdx path + blob link, SELF_HOST.md relative link + curl URL). * fix(self-host): address review round 2 — drop unused AUTH_BEARER_TOKEN, pull-by-default, doc fixes - Remove AUTH_BEARER_TOKEN everywhere (unused in app code; PRIVATE_SERVER_KEY is the real dashboard<->api key). Simplifies run-docker-full-stack.sh secret generation. - run-docker-full-stack.sh now defaults to PULLING pre-built images; add --build for source. - docs: recommend MinIO, 'a way' (not 'recommended way') to self-host, remove Direct API Access section, trim Common Commands, SMTP vars per-line, fix PRIVATE_SERVER_KEY wording. * feat(db): versioned migrations for self-host with auto-baseline - Regenerate a single clean baseline migration; db:setup now runs drizzle-kit migrate (not push). - Add db:generate / db:migrate / db:baseline scripts. - baselineMigrationsIfNeeded(): adopt an existing (push/supabase) schema into drizzle's journal so migrate won't try to recreate existing objects ("type/table already exists"). No-ops on fresh and already-tracked databases; runs automatically in db:setup before migrate. * ci(openapi): drop stale ci.yml reference in turbo-cache comment ci.yml was removed earlier in this review; the openapi workflow's turbo cache still warms from its own prior runs via restore-keys, so this is a comment-only fix. * fix(db): make baseline squash-safe for migrate-tracked databases baselineMigrationsIfNeeded returned early whenever drizzle.__drizzle_migrations had any rows. After a migration squash, an instance tracked under the old journal (pre-squash hashes) would then let `drizzle-kit migrate` re-run the consolidated baseline — whose journal `when` is newer than every old created_at — and crash on `CREATE TYPE/TABLE "..." already exists` at startup. Now, when history exists but the consolidated baseline hash is absent, insert that hash (marking it already applied) so migrate skips it. Only the baseline entry (idx 0) is reconciled, so genuinely pending future migrations still run. Verified end-to-end against Postgres: reproduces the original crash, confirms the fix no-ops migrate on a pre-squash DB, and confirms a later 0001 migration still applies. Fresh-DB and already-adopted paths unchanged. * fix(docker): add explicit id to pnpm-store cache mounts Railway's Metal builder rejects BuildKit cache mounts that omit an explicit id (it does not default id to target like standard Docker/ BuildKit do), failing the build at parse time with: dockerfile invalid: flag '--mount=type=cache,target=/pnpm/store' is missing an id argument Add id=pnpm-store to all six cache mounts across the api, dashboard, and jobs images. Harmless on Docker Desktop / CI / GitHub Actions, and required for the Railway self-host build path. * fix(docker): drop BuildKit cache mounts for Railway compatibility Railway's builder rejects cache-mount ids not prefixed with its per-service cacheKey, which can't be hardcoded in a Dockerfile shared across CI, local Docker, and three Railway services. Remove the --mount=type=cache flag (keep --store-dir /pnpm/store); the store is baked into the fetch layer and reused by the offline install, so behaviour is unchanged. Supersedes 9c4633f2c. * fix(docker): force LF for shell scripts and Dockerfiles via .gitattributes cio-api crash-looped on Railway with 'exec /app/entrypoint.sh: No such file or directory' because the entrypoint shipped with CRLF endings (Windows checkout, core.autocrlf=true) — the '#!/bin/sh\r' shebang is unresolvable in the container. Add .gitattributes pinning *.sh and Dockerfile* to LF so container-executed files are never CRLF regardless of contributor OS. * fix(db): serialize db-setup with an advisory lock to prevent baseline double-insert Concurrent db:setup runs (e.g. multiple API replicas booting together) could both pass the baseline 'already present?' check and insert duplicate rows into drizzle.__drizzle_migrations, which has no unique constraint on hash. Wrap baseline + migrate in a pg_advisory_lock held on a dedicated connection so concurrent startups serialize: the loser blocks, then sees the baseline applied and no-ops. * docs(railway): update template deploy URL to classroomio-1 --------- Co-authored-by: Rotimi Best <[email protected]>
43 KiB
ClassroomIO — Feature & Flow Map (QA Audit)
Purpose of this document: a reverse‑engineered, citation‑backed map of every feature in the ClassroomIO application, produced for QA to build test cases and record demo videos. Files are the source of truth. Every claim is backed by a file reference. Claims that could not be traced to code are marked
⚠️ UNVERIFIED. Ambiguities live in §7 Open Questions, not in the body.Reference format:
`path/to/file`→ symbol/lines — what it proves. Scope (per QA request): full depth on the user‑facing product (apps/dashboard,apps/api,apps/website,apps/embeds+ the packages that back features); a lighter Supporting Infrastructure summary for the rest. PRD folders (prd/*) are used only to cross‑check intent — code wins on conflict.
1. System Overview
What it is. ClassroomIO is an open‑source Learning Management System (LMS) for
companies — compliance/employee training, customer education, and partner
certification. It supports multi‑organization workspaces, courses/lessons/exercises,
programs (cohorts), certificates, an AI course builder + in‑lesson tutor, a public
REST API, an MCP server, and embeddable course widgets. It can be self‑hosted or
run as the cloud product. (README.md → lines 11–50 — feature summary;
README.md → 84–91 — monorepo apps.)
Tech stack (cited from manifests):
- Monorepo: pnpm + Turbo.
package.json→ root scripts/workspaces;pnpm-workspace.yaml→ packages globs (apps/*,packages/*,packages/course-app/src/*);turbo.json. - Frontend: SvelteKit + Svelte 5 + TailwindCSS.
README.md→ 52–58 ("Built With");apps/dashboard/svelte.config.js. - Backend API: Hono (TypeScript).
apps/api/src/app.ts→new Hono()app;ARCHITECHTURE.md→ layered architecture (Routes → Services → Queries). - Database: PostgreSQL via Drizzle ORM.
packages/db/src/schema.ts(~3,400 lines ofpgTable/pgEnum);packages/db/src/drizzle.ts. - Auth: Better Auth (sessions, OAuth, SSO).
apps/api/src/app.ts→ 12, 107–188 (auth.handler,/api/auth/*);packages/db/src/auth.ts. - Background jobs: BullMQ + Redis.
apps/jobs,apps/api/src/routes/admin/queues.ts(mountQueueDashboard),mediaJob/jobStep/deadLetterJobtables inschema.ts. - Object storage: S3‑compatible / MinIO.
.env.example→ 77–89 (OBJECT_STORAGE_*);README.md→ 176–191. - Billing: Polar.
apps/dashboard/src/routes/api/polar/*. - AI providers: OpenAI / Google (Gemini) / Anthropic.
.env.example→ 110–115;packages/utils/src/agent-models(perREADME.md→ 236).
High‑level architecture (ARCHITECHTURE.md → 9–51):
SvelteKit dashboard (apps/dashboard) ──RPC (Hono client)──▶ Hono API (apps/api)
│ server-side SSR proxy (.server.ts, PRIVATE_SERVER_KEY) │
│ ▼
Public org sites / embeds Services → Queries (Drizzle)
(apps/website, apps/embeds) │
▼
PostgreSQL · Redis/BullMQ · S3/MinIO
- Layered API: Routes (HTTP/validation/auth) → Services (business logic/
transactions) → Queries (pure Drizzle).
ARCHITECHTURE.md→ 44–50. - Shared Zod validation in
packages/utils/src/validation/*, used by both API and dashboard.ARCHITECHTURE.md→ 45;AGENTS.md→ 17–25. - Dashboard talks to the API through a typed Hono RPC client and an SSR auth proxy
authenticated with
PRIVATE_SERVER_KEY.README.md→ 149–156.
2. Feature Inventory
Status legend: Complete = route+service+schema present; Partial = present but gated/limited or doc claims exceed code; Stub/Planned = PRD only, little/no code.
| # | Feature | Purpose | Primary files | Status |
|---|---|---|---|---|
| 1 | Authentication & sessions | Email/password, OAuth, session mgmt | apps/api/src/app.ts:107, packages/db/src/auth.ts |
Complete |
| 2 | Signup gating | Org‑scoped/invite‑only/self‑host bootstrap rules | apps/api/src/middlewares/signup-guard.ts |
Complete |
| 3 | Onboarding & org creation | Create profile + first organization | apps/api/src/services/onboarding.ts, apps/dashboard/src/routes/(auth)/onboarding |
Complete |
| 4 | Organizations (workspaces) | Multi‑org, settings, members, plan | apps/api/src/routes/organization/organization.ts, schema.ts:1949 |
Complete |
| 5 | Team / people management | Invite teachers, assign roles | apps/api/src/routes/course/people.ts, apps/dashboard/src/lib/features/people |
Complete |
| 6 | Courses | Create/manage courses, sections, content | apps/api/src/routes/course/course.ts, schema.ts:634 |
Complete |
| 7 | Lessons & lesson editor | Lesson content, multilingual, comments | apps/api/src/routes/course/lesson.ts, .../lesson-language.ts, schema.ts:956 |
Complete |
| 8 | Exercises & questions | Quizzes, question types, sections, grading | apps/api/src/routes/course/exercise.ts, .../submission.ts, .../mark.ts, packages/question-types |
Complete |
| 9 | Course people / enrollment | Enroll students, roster | apps/api/src/routes/course/people.ts |
Complete |
| 10 | Course invites | Email + link invites with audit | apps/api/src/routes/course/invite.ts, apps/api/src/services/course/invite.ts, schema.ts:1282 |
Complete |
| 11 | Org invites | Invite org members (EMAIL/LINK) | apps/api/src/routes/invite/invite.ts, schema.ts:1384 |
Complete |
| 12 | Compliance training | Deadlines, renewals, grace, records | apps/api/src/routes/course/compliance.ts, apps/api/src/routes/internal, schema.ts:768 (courseCompletionRecord) |
Complete |
| 13 | Certificates | Issue branded certificates | packages/certificates/src/render.ts, schema.ts:850 (courseCertificateIssue) |
Complete |
| 14 | Programs (cohorts) | Group courses, goals, members, newsfeed | apps/api/src/routes/program/program.ts, schema.ts:2646 |
Complete |
| 15 | Community Q&A | Per‑course questions/answers | apps/api/src/routes/community/community.ts, schema.ts:1558 |
Complete |
| 16 | Course newsfeed | Course/program announcements + comments | apps/api/src/routes/course/newsfeed.ts, schema.ts:1605 |
Complete |
| 17 | Attendance | Track group/live‑class attendance | apps/api/src/routes/course/attendance.ts, schema.ts:321 (groupAttendance) |
Complete |
| 18 | AI Course Assistant | In‑course AI authoring/plan/edit chat | apps/api/src/routes/agent/*, packages/ai-assistant, schema.ts:3163 (aiAgentRun) |
Complete (key‑gated) |
| 19 | AI Lesson Tutor | In‑lesson learner AI helper + fair‑use caps | apps/api/src/routes/course/ai-tutor.ts, apps/api/src/routes/organization/ai-tutor.ts, schema.ts:3013 (aiTutorMessageCount) |
Complete (key‑gated) |
| 20 | AI credits / token billing | Token usage, balance, purchases | schema.ts:2919 (aiTokenUsage), apps/dashboard/src/lib/features/agent/api/credit-purchase.server.ts |
Complete (cloud billing) |
| 21 | Course import | Import from prompt/PDF/course | apps/api/src/routes/organization/course-import.ts, apps/api/src/services/course-import, schema.ts:2603 |
Complete |
| 22 | Media manager | Upload/manage video/docs/media, transcripts | apps/api/src/routes/media/media.ts, .../transcripts.ts, .../hls.ts, apps/dashboard/src/lib/features/media, schema.ts:1028 (asset) |
Complete |
| 23 | Tags | Tag groups/tags/assignments for courses | apps/api/src/routes/organization/tags.ts, schema.ts:2158 |
Complete |
| 24 | Org public site (org‑site) | Branded public course catalog/pages | apps/api/src/routes/org-site, apps/dashboard/src/routes/(org-site) |
Complete |
| 25 | Custom domains | Cloudflare custom domain mapping | apps/api/src/routes/domain/domain.ts, apps/tenant-router |
Complete |
| 26 | Course widget embed | Embeddable course catalog widget | apps/api/src/routes/widgets, apps/dashboard/src/routes/(app)/widgets, apps/embeds, schema.ts:2255 (widget) |
Complete |
| 27 | Public REST API (v1) | API‑key automation: courses, audience | apps/api/src/routes/v1/*, apps/api/src/services/v1, schema.ts:2539 (organizationApiKey) |
Complete |
| 28 | Automation / API keys | Issue mcp/api/zapier keys with scopes | apps/api/src/routes/organization/automation.ts, apps/api/src/middlewares/automation-key*.ts |
Complete |
| 29 | MCP server | @classroomio/mcp AI‑native course authoring |
packages/mcp/src/* |
Complete |
| 30 | Enterprise SSO | OIDC SSO (Okta/Google Workspace/Auth0) | apps/api/src/routes/organization/sso.ts, apps/api/src/routes/sso/discovery.ts, schema.ts:2479 (organizationSsoConfig) |
Complete (licensed) |
| 31 | Token auth | Token‑based auth policy | apps/api/src/routes/organization/token-auth.ts, schema.ts:2513 (organizationTokenAuth) |
Complete (licensed) |
| 32 | Licensing | Self‑host license‑gated features | apps/api/src/routes/license.ts, apps/api/src/middlewares/license.ts, packages/utils/src/license/* |
Complete |
| 33 | Billing (Polar) | Subscriptions + AI token packs | apps/dashboard/src/routes/api/polar/* |
Complete (cloud only) |
| 34 | Analytics | Login/page events, org/course/country daily | apps/api/src/routes/dash, apps/api/src/services/analytics, schema.ts:167–285 |
Complete |
| 35 | Email / notifications | Transactional emails via jobs | apps/api/src/routes/mail, packages/email, apps/api/src/services/jobs |
Complete |
| 36 | Background jobs | Media/email queues, BullMQ dashboard | apps/jobs, apps/api/src/routes/jobs, apps/api/src/routes/admin/queues.ts |
Complete |
| 37 | Unsplash image search | Image picker for content | apps/api/src/routes/unsplash/unsplash.ts |
Complete |
| 38 | Marketing website | classroomio.com landing | apps/website |
Complete |
| 39 | Documentation site | classroomio.com/docs | apps/docs |
Complete |
| 40 | Outbound webhooks | Emit certificate.issued, etc. |
prd/webhooks (todo) |
Stub/Planned ⚠️ |
| 41 | SCORM support | SCORM package support | prd/scorm-support (todo) |
Stub/Planned ⚠️ |
| 42 | Notification system | In‑app notifications | prd/notification-system (todo) |
Stub/Planned ⚠️ |
Items 40–42 are listed in
PRD-TRACKER.md→ 9, 16, 24 as todo; treat as not yet shipped. See §7.
3. Per‑Feature Detail
3.1 Authentication & Sessions
- Purpose: authenticate users (email/password + OAuth) and attach org roles to the session.
- How it works: All
/api/auth/*requests are handled by Better Auth viaauth.handler.apps/api/src/app.ts→ 107–188. A global middleware resolves the session on every request and setsuser,session, andorgRolesinto context.apps/api/src/app.ts→ 60–97. The app rebuilds the request host fromX-Forwarded-Host/-Proto(Cloudflare proxy) and uses a Redis "oauth handoff" to shrink large OAuth callback cookies into a short token.apps/api/src/app.ts→ 110–173. - User flow: Login page → submit email/password (or OAuth) → Better Auth sets
session cookie → dashboard reads
/session.apps/dashboard/src/routes/(auth)/login;apps/api/src/app.ts→ 189–199 (/session). - Key files:
apps/api/src/app.ts,packages/db/src/auth.ts,packages/db/src/auth/hooks/create-profile.ts. - Edge cases: OAuth cookie > 500 chars routed through Redis token (
app.ts:160);getSessionfailures fall through as anonymous (app.ts:71–84); session carriesorgRolesmap used by RBAC middlewares (app.ts:88).
3.2 Signup Gating (signupGuard)
- Purpose: enforce org‑level and deployment‑level signup rules server‑side.
- How it works:
apps/api/src/middlewares/signup-guard.ts→ 129–159. With acio-org-idheader: rejects whenorg.disableSignup(403SIGNUP_DISABLED, line 148) or whensettings.signup.inviteOnlyand the email lacks an active invite (403INVITE_REQUIRED, lines 97–113). Without the header: cloud passes through; self‑hosted requires org context unless no org exists yet (bootstrap) or the email already belongs to the org. Lines 75–91 (canProceedWithoutOrgContext). - User flow: Signup form → POST
/api/auth/sign-up/*(guarded,app.ts:106) → allowed /SIGNUP_DISABLED/INVITE_REQUIRED/ORG_CONTEXT_REQUIRED. - Edge cases (tester probes): invite‑only org without invite; disabled signup;
self‑hosted with no
cio-org-idand an existing org; unparseable body → 400INVALID_REQUEST.
3.3 Onboarding & Organization Creation
- Purpose: create the user profile and first organization, then send a welcome email.
- How it works:
apps/api/src/services/onboarding.ts.createOrganizationWithOwnervalidates site‑name uniqueness (409SITENAME_EXISTS, lines 36–40), creates org + admin member in a transaction (lines 44–61), and on self‑hosted assigns the org anENTERPRISEplan with providerselfhosted(lines 64–77). Self‑hosted blocks a second org (403, lines 28–33).completeOnboardingenqueues awelcomeemail (lines 121–134). - User flow: Signup →
(auth)/onboarding→ enter name/org/site name → org+admin created → dashboard. - Edge cases: duplicate site name (409 + Postgres
23505fallback, lines 93–95); second‑org attempt on self‑host; profile not found (404).
3.4 Organizations (Workspaces)
- Purpose: the top‑level tenant; owns courses, members, settings, plan, branding.
- How it works:
apps/api/src/routes/organization/organization.ts(CRUD + settings);organizationtableschema.ts:1949(siteName, settings, branding,disableSignup). Members live inorganizationmember(schema.ts:1754) withroleIdFK torole. - User flow: Org switcher → settings → update name/site/branding/signup policy.
apps/dashboard/src/lib/features/ui/sidebar/org-sidebar/org-switcher.svelte;apps/dashboard/src/lib/features/settings. - Edge cases: unique
siteName; self‑hosted = single org (see §5); org settings drive signup gating (§3.2).
3.5 Courses, Sections & Content
- Purpose: author and deliver courses (self‑paced, live‑class, compliance, public).
- How it works: course sub‑routers under
apps/api/src/routes/course/—course.ts,section.ts,content.ts,lesson.ts,exercise.ts,people.ts,newsfeed.ts,attendance.ts,compliance.ts,mark.ts,submission.ts,invite.ts,lesson-language.ts,payment-request.ts,presign.ts,ai-tutor.ts,katex.ts(index.tscomposes them). Course type enumSELF_PACED|LIVE_CLASS|COMPLIANCE|PUBLIC(schema.ts:26). Course rowschema.ts:634. - User flow: Courses → New course → add sections/lessons → add exercises → publish →
students enroll.
apps/dashboard/src/routes/(app)/courses/[id]. - Edge cases: course type drives compliance/public behavior; locale enum (
schema.ts:27, 10 languages) drives multilingual lessons.
3.6 Lessons & Multilingual Editor
- Purpose: lesson content with notes/slides/video, comments, translations.
- How it works:
lesson(schema.ts:956),lessonComment(schema.ts:516),lessonCompletion(schema.ts:1490),lessonLanguage+…History(schema.ts:1706,1732). Routes:apps/api/src/routes/course/lesson.ts,.../lesson-language.ts. PRDsprd/lesson-edit [DONE],prd/lessons-list [DONE]confirm intent. - User flow: Open lesson → edit content → add translation → student marks complete.
- Edge cases: language history retained; completion tracked per student.
3.7 Exercises, Question Types & Grading
- Purpose: quizzes/assessments with multiple question types and grading.
- How it works:
exercise(schema.ts:1175),exerciseSection(schema.ts:1217),question/questionType/questionAnswer(schema.ts:1803–1913),submission/submissionstatus(schema.ts:571,617),exerciseTemplate(schema.ts:2401). Routesexercise.ts,submission.ts,mark.ts. Shared types inpackages/question-types. PRDs:exercise-question-types,exercise-sections [DONE],video-recording-question-type [DONE]. - User flow: Author exercise → add questions (incl. video‑recording type) → student submits → tutor/admin marks → status recorded.
- Edge cases: section‑based exercises; video‑recording answers (media pipeline);
prd/selective-exercise-assignmentis todo (assign specific exercises to specific students) — verify it is not yet available.
3.8 Enrollment, Course Invites & Secure Invites
- Purpose: add students to courses by email or shareable link, with audit trail.
- How it works:
apps/api/src/services/course/invite.ts;courseInvite(schema.ts:1282) +courseInviteAudit(schema.ts:1342); event‑type enumschema.ts:53. Org invites:organizationInvite(schema.ts:1384, EMAIL/LINK enumschema.ts:71) +…Audit(schema.ts:1448), routesapps/api/src/routes/invite/invite.ts. PRDsecure-student-invites [DONE]. - User flow: Course → People → Invite (email or link) → invitee accepts via
apps/dashboard/src/routes/invite/[hash]or/invite/link/[hash]→ enrolled. - Edge cases: invite expiry/custom expiry (
AGENTS.md:231referencescustomExpiresAt); link vs email; audit events recorded; invite‑only signup interplay (§3.2).
3.9 Compliance Training
- Purpose: deadline tracking, renewals/retake intervals, grace periods, completion records.
- How it works:
apps/api/src/routes/course/compliance.tsand internal compliance routerapps/api/src/routes/internal(AGENTS.md:87–98);courseCompletionRecord(schema.ts:768),courseCompletionNotificationEvent(schema.ts:824),courseCertificateIssue(schema.ts:850). Course typeCOMPLIANCE(schema.ts:26). PRDcompliance-training-platform [DONE]. - User flow: Mark course as compliance → set deadlines/renewal → learners complete → completion record + certificate; renewal/grace events tracked.
- Edge cases: grace periods, renewal intervals, waivers (per README 21), notification events.
3.10 Certificates
- Purpose: issue branded completion certificates with custom IDs.
- How it works:
packages/certificates/src/render.ts+templates/render the certificate;courseCertificateIssue(schema.ts:850) records issuance. - User flow: Learner completes course → certificate issued → downloadable/branded.
- Edge cases: custom certificate IDs (README 21); template selection.
3.11 Programs (Cohorts) & Goals
- Purpose: group courses into cohorts with goals, members, and progress tracking.
- How it works:
apps/api/src/routes/program/program.ts; tablesprogram(schema.ts:2646),programCourse(2677),programMember(2704),programNewsfeed+Comment(2739,2767),programGoal/…Assignment(2829,2878) with goal‑type/ deadline/status enums (2803–2828), status enumACTIVE|INACTIVE|ARCHIVED(2644). Middlewaresprogram-member.ts,program-team-member.ts. PRDprograms [DONE]. - User flow: Create program → add courses + members → set goals/deadlines → track
progress → cohort newsfeed.
apps/dashboard/src/routes/(app)/programs/[id]. - Edge cases: goal deadline kinds; assignment status; archived programs.
3.12 Community Q&A & Newsfeed
- Purpose: per‑course discussion (questions/answers) and announcements.
- How it works:
apps/api/src/routes/community/community.tswith route‑specificmiddlewares/question-author-or-team(ARCHITECHTURE.md:241–252); tablescommunityQuestion/communityAnswer(schema.ts:1558,1522),courseNewsfeed/…Comment(1605,1632). Comment‑author middlewaresnewsfeed-comment-author-or-team.ts,program-newsfeed-comment-author-or-team.ts. - User flow: Course → Community → ask/answer; team can moderate. Newsfeed posts + comments at course and program level.
- Edge cases: author‑or‑team authorization (only author or course team can edit/delete).
3.13 AI Course Assistant (authoring chat)
- Purpose: in‑course AI that generates outlines, lesson content, and edits.
- How it works:
apps/api/src/routes/agent/{agent,history,runs}.ts;packages/ai-assistant; run tracking inaiAgentRun/…Step/…Event(schema.ts:3163,3225,3259), conversation/model context/documents (3083,3106,3138). Disabled until a provider key is set;GET /agent/statusflips toenabled:truewhen any of OPENAI/GOOGLE/ ANTHROPIC key is present.README.md:223–238. Selected model persisted inlocalStorage(classroomio-ai-chat-model), provider resolved server‑side; missing key → 503AI_NOT_CONFIGURED(README.md:236). - User flow: Course → AI button (only shown when enabled) → chat to build/edit course.
- Edge cases: no key → AI button hidden / 503; per‑model provider resolution; optional
Tinybird observability (
TINYBIRD_TOKEN).
3.14 AI Lesson Tutor + Fair‑Use Caps
- Purpose: in‑lesson AI helper for learners, with usage caps.
- How it works:
apps/api/src/routes/course/ai-tutor.ts,apps/api/src/routes/organization/ai-tutor.ts;aiTutorMessageCount(schema.ts:3013)aiTutorCapEvent(3050). Settings UIapps/dashboard/src/lib/features/ai-tutor-settings. PRDai-tutor-fair-use.
- User flow: Learner in lesson → ask tutor → counts toward cap → cap event when exceeded.
- Edge cases: per‑org cap configuration; cap‑exceeded behavior.
3.15 AI Credits / Token Billing
- Purpose: meter AI token usage and sell token packs.
- How it works:
aiTokenUsage(schema.ts:2919),aiCreditBalance(2950),aiCreditPurchase(2974); purchases recorded by Polarorder.paidwebhook →CreditPurchaseApiServer.recordPurchase.apps/dashboard/src/routes/api/polar/webhook/+server.ts→ 33–82; token‑pack constants@cio/utils/plans(TOKEN_PACK.TOKENS_PER_UNIT, line 53). - User flow: Buy tokens (
/api/polar/buy-tokens) → Polar checkout → webhook credits org balance. - Edge cases: cloud only (Polar); quantity/metadata parsing; UUID validation of
triggeredBy(lines 61–66).
3.16 Course Import
- Purpose: create courses from a prompt, PDF, or existing course.
- How it works:
apps/api/src/routes/organization/course-import.ts,apps/api/src/services/course-import;courseImportDraft(schema.ts:2603) with source enumprompt|pdf|course(schema.ts:29) and draft statusDRAFT|PUBLISHED|ARCHIVED(30). - User flow: Org → Import course → choose source → review draft → publish.
- Edge cases: draft lifecycle; PDF parsing pipeline (jobs/media).
3.17 Media Manager (video/docs, transcripts, HLS)
- Purpose: upload and manage media; transcode video to HLS; generate transcripts.
- How it works: routes
media/media.ts,transcripts.ts,hls.ts,course/presign.ts(S3 presigned uploads). Tablesasset/assetUsage(schema.ts:1028,1086),mediaJob/jobStep/mediaTranscript/deadLetterJob(schema.ts:3297,3357,3393,3447),videoTranscripts(916). Jobs processed byapps/jobs. Dashboardapps/dashboard/src/lib/features/media. PRDmedia-manager [DONE]. Storage via MinIO/S3 (.env.example:77–89). - User flow: Media → upload → background transcode/transcribe → use asset in lesson.
- Edge cases: job retries / dead‑letter; HLS cross‑origin headers (
app.ts:48–54); Google Drive picker optional (.env.example:99–101).
3.18 Tags
- Purpose: categorize courses via tag groups/tags.
- How it works:
apps/api/src/routes/organization/tags.ts;tagGroup/tag/tagAssignment(schema.ts:2158,2188,2225). PRDtags-feature [DONE]. - User flow: Org → Tags → create groups/tags → assign to courses → filter catalog.
3.19 Org Public Site (org‑site) & Theming
- Purpose: branded public catalog and course landing pages for an org.
- How it works: API
apps/api/src/routes/org-site(mounted/org-site/course,app.ts:217); dashboard public routesapps/dashboard/src/routes/(org-site)/{course,courses}. Theme picker PRDorg-landing-page-theme-picker [DONE]. - User flow: Visitor → org public site → browse courses → view course → enroll/sign up.
- Edge cases: self‑host vs cloud routing differences (see §5); org branding/theme.
3.20 Custom Domains
- Purpose: map a customer's own domain to their org site.
- How it works:
apps/api/src/routes/domain/domain.ts(mounted/domain,app.ts:203); routing appapps/tenant-router. PRDcustom-domain-cloudflare [DONE]. - User flow: Org settings → add custom domain → Cloudflare mapping → site served on domain.
3.21 Course Widget Embed
- Purpose: embed an org's course catalog on any external website.
- How it works: API
apps/api/src/routes/widgets(mounted/widgets,app.ts:221); dashboardapps/dashboard/src/routes/(app)/widgets/[widgetId]+widget-preview; build artifactsapps/embeds(embeds:publish,README.md:256–257). Tableswidget/widgetCourse/widgetVersion(schema.ts:2255,2305,2336) with status/layout/ selection enums (42–52). PRDcourse-widget-embed [DONE]. - User flow: Widgets → create widget → choose courses/layout → publish → copy embed
snippet (
embed-preview.html). - Edge cases: draft vs published widget; manual vs published course selection mode.
3.22 Public REST API (v1) & Automation Keys
- Purpose: programmatic access for integrations/automation.
- How it works:
apps/api/src/routes/v1/{courses,audience}.tsmounted at/public-api/v1(app.ts:218,v1/index.ts). Auth viaautomationKeyMiddleware+automationKeyScopesMiddleware(['public_api:*'])on every route; OpenAPI documented viahono-openapi. Courses endpoints: list/create/get/update/delete, list students, export, get/put structure (v1/courses.ts). Keys:organizationApiKey(schema.ts:2539, typemcp|api|zapier), usage tracked inorganizationAutomationUsage(2574); managed viaapps/api/src/routes/organization/automation.ts. PRDpublic-api [DONE]. - User flow: Org → API keys → create key with scopes → call
/public-api/v1/...with key. - Edge cases: scope enforcement (403 on missing scope); usage categories
read|write|publish(schema.ts:32); per‑org isolation viaorgIdfrom key context.
3.23 MCP Server
- Purpose: AI‑native course authoring through the Model Context Protocol.
- How it works:
packages/mcp/src/{index.ts,api-client.ts,config.ts,tools/}; published as@classroomio/mcp(README.md:258). Talks to the public API using an automation key. PRDmcp-course-authoring [DONE]. - User flow: Configure MCP client with API key → AI tools create/update courses.
3.24 Enterprise SSO & Token Auth (licensed)
- Purpose: OIDC SSO and token‑based auth for enterprise self‑host.
- How it works:
apps/api/src/routes/organization/sso.ts,apps/api/src/routes/sso/discovery.ts(mounted/organization/sso,/sso,app.ts:211,213);organizationSsoConfig(schema.ts:2479), provider enumOKTA|GOOGLE_WORKSPACE|AUTH0(schema.ts:2444),organizationAuthPolicy(2447). Token auth:apps/api/src/routes/organization/token-auth.ts,organizationTokenAuth(2513). License‑gated on self‑host viarequireLicense('sso'|'token-auth')(apps/api/src/middlewares/license.ts). PRDenterprise-sso [DONE]. - User flow: Org → SSO settings → register IdP (discovery fetches
.well-known/openid-configuration) → users sign in via IdP. - Edge cases: trusted OIDC origins allow‑listed (
packages/db/src/constants.ts:14–24); cloud has no license restriction (see §5).
3.25 Licensing
- Purpose: gate Enterprise self‑host features behind a license key.
- How it works:
apps/api/src/routes/license.ts(mounted/license,app.ts:209),apps/api/src/services/license.ts(isFeatureLicensed), feature idssso,token-auth,no-tracking(packages/utils/src/license/constants.ts:8–10), plan→feature mappingpackages/utils/src/license/plan-features.ts. Dashboard sideapps/dashboard/src/lib/features/license/api/license.svelte.ts(licenseApi.hasAccess). - User flow: Set
LICENSE_KEY(.env.example:117–118) → licensed features unlock. - Edge cases:
requireLicenseis a no‑op on cloud (license.ts:15–19).
3.26 Billing (Polar) — cloud
- Purpose: paid subscriptions and AI token packs.
- How it works:
apps/dashboard/src/routes/api/polar/{subscribe,buy-tokens,portal,webhook}. Webhook handlesorder.paid(token packs → credit balance) andsubscription.*(create/update/cancel org plan).webhook/+server.ts→ 30–168. Plans enumEARLY_ADOPTER|ENTERPRISE|BASIC(schema.ts:28), org plan stored inorganizationPlan(schema.ts:1667). - User flow: Upgrade → Polar checkout → webhook activates/updates plan; manage via portal.
- Edge cases: cloud only; subscription cancel →
cancelOrgPlan; webhook secretPOLAR_WEBHOOK_SECRETrequired.
3.27 Analytics
- Purpose: usage analytics for orgs/courses.
- How it works:
apps/api/src/routes/dash+apps/api/src/services/analytics; tablesanalyticsLoginEvents,analyticsPageEvent,analyticsOrgDaily,analyticsCourseDaily,analyticsCountryDaily(schema.ts:167–285). Dashboardapps/dashboard/src/lib/features/analytics. Client tracking (PostHog/Umami) gated by deployment mode / license (§5). - User flow: Org → Analytics dashboard → view login/page/course metrics.
3.28 Email / Notifications & Background Jobs
- Purpose: transactional email and async processing.
- How it works:
apps/api/src/routes/mail, templates inpackages/email, enqueued viaenqueueTransactionalEmail(apps/api/src/services/jobs,onboarding.ts:128). Workers inapps/jobs; BullMQ dev dashboard at/admin/queues(apps/api/src/routes/admin/queues.ts,app.ts:232–234). Email providers: Zoho/ZeptoMail or SMTP (.env.example:62–74). - User flow: trigger (signup, invite, completion) → email queued → worker sends.
- Edge cases: idempotency keys (
onboarding.ts:133); dead‑letter jobs (schema.ts:3447). - Note: an in‑app notification system is
prd/notification-systemtodo — not shipped.
4. Roles & Responsibilities
Org roles are integers: ADMIN=1, TUTOR=2, STUDENT=3
(packages/utils/src/constants/roles.ts:1–4). They are stored on
organizationmember.roleId (schema.ts:1767, FK to role table schema.ts:1913) and
surfaced into the session as an orgRoles map keyed by org id (apps/api/src/app.ts:88).
RBAC middlewares read orgRoles[orgId] (org id from the cio-org-id header) — no
per‑request DB lookup (apps/api/src/middlewares/org-team-member.ts:5–53).
| Role | Can do | Cannot do | Enforced by |
|---|---|---|---|
| ADMIN (1) | Full org control: settings, members, billing, courses, SSO, API keys, all team actions | — | org-admin.ts, org-team-member.ts (admin∈team) |
| TUTOR (2) | Team actions: author/manage courses, grade, moderate community/newsfeed | Org admin‑only ops (org settings, billing, member roles) | org-team-member.ts:44 (ADMIN or TUTOR), course-team-member.ts |
| STUDENT (3) | Enroll, learn, submit exercises, post community Q&A, use tutor | Authoring/grading/team/admin actions | absence of team/admin role → 403 |
| Guest / anonymous | Public org sites, public courses, embeds, public API (with key) | Any authed action | session = null path (app.ts:77–84); public CORS paths (app.ts:60–67) |
Resource‑scoped middlewares (apps/api/src/middlewares/):
authMiddleware— requires a session (auth.ts).orgMemberMiddleware— must be org member;orgTeamMemberMiddleware— ADMIN|TUTOR;orgAdminMiddleware— ADMIN only (ARCHITECHTURE.md:233–237).courseMemberMiddleware/courseTeamMemberMiddleware— course‑level membership/team.programMemberMiddleware/programTeamMemberMiddleware— program‑level.apiKeyMiddleware/automationKeyMiddleware(+automationKeyScopesMiddleware) — machine auth for webhooks/public API;authOrApiKeyMiddleware,authOrAutomationKeyMiddleware, and*-or-automation-key/*-or-api-keyvariants allow either a session or a key.requireLicense(feature)— self‑host license gate (license.ts).workspaceNotReadonlyMiddleware— blocks writes on read‑only workspaces (workspace-not-readonly.ts).signupGuard— signup rules (§3.2).rateLimiter— global rate limit (app.ts:98).- Route‑specific author/team guards:
question-author-or-team,newsfeed-comment-author-or-team,program-newsfeed-comment-author-or-team.
Public API scopes: automation keys carry scopes like public_api:*, enforced per route
(apps/api/src/routes/v1/courses.ts:47); usage categorized read|write|publish
(schema.ts:32).
5. Deployment Modes — Self‑Hosted vs Cloud
The single switch is PUBLIC_IS_SELFHOSTED (=true ⇒ self‑hosted). Source of truth:
.env.example:26 (Docker stack), apps/dashboard/.env.example, and the local‑dev value
apps/dashboard/.env (PUBLIC_IS_SELFHOSTED=true, README.md:155). The flag is read in
both the API/db layer (process.env.PUBLIC_IS_SELFHOSTED) and the dashboard
($env/static/public). Every behavioral difference below is traced to a flag check.
5.1 Self‑Hosted mode (PUBLIC_IS_SELFHOSTED=true)
- Setup: Docker Compose full stack —
cp .env.example .envthen./run-docker-full-stack.sh(auto‑generatesPRIVATE_SERVER_KEY,BETTER_AUTH_SECRET);README.md:240–249,docker-compose.yaml,docker/Dockerfile.{api,dashboard},docker/docs/SELF_HOST.md. Bundled Postgres, Redis, MinIO. - Single organization: org creation blocks a second org
(
apps/api/src/services/onboarding.ts:28–33); the new org is auto‑assigned anENTERPRISEplan with providerselfhosted(onboarding.ts:64–77). The "add org" UI is hidden (apps/dashboard/src/routes/(app)/org/[slug]/+layout.svelte:29–31) and the org‑site→LMS redirect is skipped (.../+layout.server.ts:16). PRDself-host-single-org.md. - Signup: requires org context unless bootstrapping the first org or the email already
belongs to the org (
signup-guard.ts:75–91). - Licensing: Enterprise features (
sso,token-auth,no-tracking) are gated byLICENSE_KEY—requireLicenseactively checks (middlewares/license.ts:15–22);.env.example:117–118. - Tracking: cloud analytics (PostHog/Umami) are disabled
(
apps/dashboard/src/lib/utils/functions/appSetup.ts:23–27); ano-trackinglicense also suppresses tracking (appSetup.ts:29–37). - URLs/links:
BASE_URLand invite/email links useDASHBOARD_ORIGINwhen self‑hosted (packages/db/src/constants.ts:36–37;.env.example:32–35). Dashboard SSR calls the API internally viaPRIVATE_SERVER_URL(.env.example:28–30). - Storage/email: MinIO + Zoho/SMTP from
.env(.env.example:62–89).
5.2 Cloud mode (PUBLIC_IS_SELFHOSTED ≠ true)
- Multi‑org: users can create/switch multiple orgs (no single‑org block); "add org" UI
shown (
+layout.svelte:29). - Billing: Polar subscriptions + AI token packs are active
(
apps/dashboard/src/routes/api/polar/*); plan set via subscription webhooks (webhook/+server.ts). License gate is a no‑op — cloud has no license restrictions (middlewares/license.ts:15–19). - Tracking: PostHog/Umami/UserJot analytics enabled (
appSetup.ts:17–27). - Signup: org context optional unless org settings restrict it (
signup-guard.ts:132–139). - Infra: hosted Postgres/Redis/S3; dashboard deployed on Cloudflare Workers
(
apps/dashboard/wrangler.jsonc,svelte.config.js).
5.3 Mode‑exclusive features (summary)
| Concern | Self‑Hosted | Cloud |
|---|---|---|
| Number of orgs | One only (onboarding.ts:28) |
Many |
| Enterprise SSO / token‑auth | Requires LICENSE_KEY (license.ts) |
Always allowed (no license) |
| Billing (Polar) | Not used (plan = selfhosted ENTERPRISE) | Subscriptions + token packs |
| Analytics tracking | Off (unless… still off) (appSetup.ts:24) |
On |
| API base / links | DASHBOARD_ORIGIN (constants.ts:37) |
PUBLIC_SERVER_URL |
| Add‑org modal | Hidden (+layout.svelte:29) |
Shown |
Both modes exist in source and share one codebase; neither mode is
⚠️ NOT FOUND. The product is one app with flag‑driven divergence.
6. End‑to‑End Flow Diagrams (text)
A. Signup → Onboarding → Workspace
Login/Signup (apps/dashboard/.../(auth)/signup)
→ POST /api/auth/sign-up/* [signupGuard] (app.ts:106; signup-guard.ts)
→ Better Auth creates user + profile (auth.ts; create-profile.ts)
→ (auth)/onboarding: createOrganizationWithOwner (onboarding.ts:19)
· self-host: block 2nd org, assign ENTERPRISE plan
→ completeOnboarding → enqueue 'welcome' email (onboarding.ts:121)
→ Dashboard /lms or /org/[slug]
B. Author a course
Courses → New course (course.ts)
→ add sections (section.ts) → lessons (lesson.ts) + translations (lesson-language.ts)
→ add exercises/questions (exercise.ts, question-types)
→ [optional] AI Assistant builds content (agent/*; requires provider key)
→ publish → appears on org site / widget
C. Student learns → certificate (compliance)
Invite (course/invite.ts) → accept /invite/[hash]
→ enroll (people.ts) → study lessons → mark complete (lessonCompletion)
→ submit exercises (submission.ts) → tutor grades (mark.ts)
→ completion recorded (courseCompletionRecord schema.ts:768)
→ certificate issued (packages/certificates; courseCertificateIssue schema.ts:850)
→ compliance: deadlines/renewals tracked (course/compliance.ts; internal/*)
D. Public API automation (integration)
Org → create automation key with scopes (organization/automation.ts; organizationApiKey)
→ external client: GET/POST /public-api/v1/courses
[automationKeyMiddleware + automationKeyScopesMiddleware('public_api:*')]
→ service resolves orgId from key → returns course data (v1/courses.ts; services/v1)
E. Cloud upgrade / buy AI tokens (cloud only)
Upgrade or Buy tokens (api/polar/subscribe | buy-tokens)
→ Polar checkout
→ Polar webhook (api/polar/webhook/+server.ts)
· subscription.created → createOrgPlan
· order.paid (token_pack) → recordPurchase → aiCreditBalance
7. Open Questions & Gaps
- Outbound webhooks — doc vs code mismatch.
README.md:42–43advertises "REST API + Webhooks … receive events (certificate.issued,enrollment.completed)", but the only webhook code is inbound Polar billing (apps/dashboard/src/routes/api/polar/webhook) andprd/webhooksis todo (PRD-TRACKER.md:24). No outbound event‑emission/delivery system was found inapps/api/srcorpackages/db/src. Code wins: outbound webhooks appear unimplemented. QA should not test event delivery as a shipped feature. ⚠️ - SCORM support — README does not promise it, but
prd/scorm-supportis todo (PRD-TRACKER.md:16). No SCORM code located. ⚠️ Treat as not shipped. - In‑app notification system —
prd/notification-systemis todo (PRD-TRACKER.md:9). Transactional email exists (§3.28); in‑app notifications appear unbuilt. Verify before demoing. ⚠️ - Selective exercise assignment —
prd/selective-exercise-assignmentis todo (PRD-TRACKER.md:15). Assigning specific exercises to specific students may not exist; confirm againstexercise.tsbehavior. ⚠️ - Course templates / cohorts / import variants —
prd/course-templates,prd/course-cohorts,prd/course-importare todo (PRD-TRACKER.md:10,12,13), yet related schema exists (exerciseTemplateschema.ts:2401;courseImportDraftschema.ts:2603; programs are DONE). Scope of what's actually wired end‑to‑end vs. schema‑only should be verified per feature before writing test cases. ⚠️ - Anthropic in AI picker — code supports Anthropic, but it is "not currently in the
picker UI" (
README.md:231,.env.example:112). The picker exposes Gemini + GPT‑4o; note the.env.example:111"Gemini 3 Flash" vsREADME.md:228"Gemini 2.5 Flash" wording inconsistency — verify the actual model id inpackages/utils/src/agent-models. - Two operating sub‑modes within self‑host (single‑org bootstrap vs. licensed
Enterprise) interact with
signupGuardandrequireLicense. Exact UX whenLICENSE_KEYis absent on self‑host (SSO/token‑auth hidden vs. 403) should be confirmed in the dashboardlicensefeature before recording a self‑host SSO demo. - Tenant‑router / domain routing (
apps/tenant-router,/domain) was inventoried but not deeply traced (infra scope). Custom‑domain happy‑path is PRD‑DONE; failure/SSL provisioning states are unverified here.
Appendix — Supporting Infrastructure (light)
These exist in source but are infra/dev tooling rather than demoable product features:
apps/jobs— BullMQ worker process; consumes media/email queues (paired withapps/api/src/routes/jobs,mediaJob/jobSteptables). Dev queue UI at/admin/queues(apps/api/src/routes/admin/queues.ts).apps/tenant-router— multi‑tenant/custom‑domain request routing (backs §3.20).apps/course-app/packages/course-app— course‑app runtime modules (pnpm-workspace.yaml:4).apps/docs— documentation site (pnpm docs:dev).apps/website— marketing site (pnpm website:dev).packages/core— shared config incl. env access (packages/core/src/config/env.ts, used bysignup-guard.ts,license.ts,onboarding.ts).packages/storybook— UI component stories (pnpm storybook:dev).packages/analytics— analytics helpers (paired withapps/api/src/services/analytics).packages/tsconfig— shared TS config.packages/ui— shared Svelte component library (@cio/ui); theme classes requireui:prefix in dashboard (AGENTS.md:313).packages/utils— shared Zod validation, plans, license, agent‑models, constants.packages/db— Drizzle schema/queries/migrations/seed;pnpm --filter @cio/db db:setup:seedseeds demo data and the demo login[email protected]/123456(README.md:215–219).
How this audit was verified
Every section traces to files read during the audit: README.md, ARCHITECHTURE.md,
AGENTS.md, .env.example, PRD-TRACKER.md, apps/api/src/app.ts,
packages/db/src/schema.ts, the role/RBAC middlewares, signup-guard.ts,
onboarding.ts, appSetup.ts, the Polar webhook, the v1 public‑API router, and the
license constants/middleware. No application was run and no tests were executed —
functional testing is deferred to the QA phase per the audit brief.