- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
The catalog is the platform's shared curriculum store. One copy of every subject,
chapter, and lesson lives in global tables with no schoolId; every school
references that same content through small bridge rows instead of cloning it.
Teacher- and school-contributed content (videos, materials, questions, books)
flows through SaaS review and enriches the catalog for everyone.
This page is the single hub for the catalog. The full CDN/asset reference lives separately at CDN & Assets.
Overview
The whole system rests on one mental model: there are global catalog
tables (shared, no tenant) and schoolId-scoped bridge rows that connect a
tenant to that shared content. A school never copies the catalog — it points at it.
prisma/models/— catalog data modelcatalog.prisma— Subject — content rootchapter.prisma— Chapterlesson.prisma— Lessoncurriculum.prisma— Curriculumvideo.prisma— Video, VideoPurchase, Attachmentqbank.prisma— Question, ExamQuestionexam.prisma— Exam, ExamTemplatematerial.prisma— Materialassignment.prisma— Assignmentbook.prisma— Booktextbook.prisma— Textbookdocument.prisma— Documentproposal.prisma— Proposalbridge.prisma— SubjectSelection, ContentOverride, InstructorPreference, BookSelectionenrollment.prisma— Enrollment, LessonProgress, SubjectCertificateacademic.prisma— AcademicLevel, AcademicGrade, AcademicStreamprisma/seeds/catalog/— load content into the DBindex.ts— orchestrator: seedCatalog (US + full SD) + seedFullCatalogengine.ts— seedSubjectsOnly — the shallow nationals' shared seederregistry.ts— all 12 Curriculum records + curriculumId backfillus.ts— US — the US-curriculum source backbonetree.ts— the catalog repo (../catalog) → sd · gb · cbse · ib-dp · caie-igcse: subjects, chapters, lessons, questions, exams — ids preservedsa.ts · eg.ts · ae.ts · qa.ts · kw.ts · jo.ts— subjects-only nationals (split from world.ts; graduate to deep trees)concepts.ts— concept images to S3banners.ts— concept banners to S3 (owned, shared)lesson-covers.ts— per-lesson/chapter covers to S3 (US, legacy)books.ts— global catalog bookscontent.ts— materials, exams, questionsvideos.ts— lesson videosexam-templates.ts— exam blueprints + templatesdemo.ts— demo-school structure + bridgessrc/components/catalog/— shared core — every surface imports from hereconcepts-data.ts— 23 concepts, colors, nearestConcept, exact-match maps + pools — single sourcesetup.ts— provisions a school onto the catalogprovision.ts— schedule/sections/timetable/library provisioning + the repair doctorimage.ts— Sharp pipeline + S3 uploadimage-url.ts— resolve image URLs (CloudFront)src/components/saas-dashboard/catalog/— platform authoring (DEVELOPER)src/components/school-dashboard/listings/subjects/catalog/— school adoption + contributionsrc/components/lumos/data/catalog/— LMS read queries../catalog/— github.com/databayt/catalog — the curriculum repo; mirrors cdn.databayt.org/catalog/sd/ · gb/ · us/ · cbse/ · ib-dp/ · caie-igcse/— <cur>/<grade>/<subject>/{structure,qbank,exams}.json, c<N>/l<N>/ — read by tree.ts (us is not adopted)scripts/migrate/renames.json— pre-catalog slug → catalog id (tree.ts adopts rows by it)public/subjects/concepts/— 23 source concept imagesscripts/us-curriculum/us-inventory.json— US source datasudan-data/— Sudan source data + TOC + PDFs- Global catalog (
catalog_*tables, noschoolId) — the curriculum hierarchy (Subject→Chapter→Lesson) plus the content attached to it (Exam,Question,Material,Assignment,Video,Book,Quiz,Document,Image,Textbook) andCurriculumrecords. - Bridge rows (
school_*tables, carryschoolId) —SubjectSelection,ContentOverride,InstructorPreference,BookSelection. These tie a tenant to the shared content; everything school-facing reads through them. - Why — at 1,000 schools the shared model stores ~240 subjects instead of 220k, and every contribution enriches all schools.
| Approach | Data for 1,000 schools × ~240 subjects | Sharing |
|---|---|---|
| Per-school clone | 220k subjects, 800k chapters, 4M lessons | None |
| Shared catalog | ~240 subjects, ~800 chapters, ~4k lessons + bridge records | Every contribution enriches all schools |
Status at a glance
| Capability | State |
|---|---|
| Global catalog + bridge model | Wired — production |
| Curriculum breadth | 12 curricula wide, 6 deep (US, SD, GB, CAIE-IGCSE, IB-DP, CBSE) |
| Per-curriculum academic structure | Wired — academic-config.ts (the old "Sudanese-for-everyone" gap is fixed) |
Provisioning (setupCatalogForSchool) | Wired — infer + 4-step fallback + lazy self-heal |
| Proposal → approval (opt-in) | Wired this session — propose, operator queue, publish, notify, pinned picker |
| Approval publishes content | Fixed — approval now sets status: PUBLISHED (was a DRAFT-invisible gap) |
| Paid videos | Wired — VideoPurchase, signed CloudFront, null-URL gating |
| Paid exams/questions | Deferred — fields exist, purchase flow not built |
| Subject thumbnails | Complete — all 12 curricula via shared concepts |
| Banners / textbook covers | Partial / missing (see CDN & Assets) |
| Content seed idempotency | Fixed this session — re-runs skip existing rows |
Architecture
Catalog models use bare names mapped to catalog_* tables; bridge models map
to school_*. The Prisma client accessor is the camelCase of the model name
(db.subject, db.subjectSelection).
Global catalog models
| Model | Table (@@map) | Accessor | Holds |
|---|---|---|---|
Subject | catalog_subjects | db.subject | Root of the hierarchy; slug (unique), curriculum, concept, denormalized counts |
Chapter | catalog_chapters | db.chapter | subjectId FK, sequenceOrder |
Lesson | catalog_lessons | db.lesson | chapterId FK, sequenceOrder, durationMinutes |
Video | lesson_videos | db.video | videoUrl, provider, visibility, price, approval |
Exam | catalog_exams | db.exam | Catalog exam (≠ school SchoolExam) |
ExamQuestion | catalog_exam_questions | db.examQuestion | Exam ↔ Question junction with order/points |
Question | catalog_questions | db.question | Catalog question bank (≠ school QuestionBank) |
Material | catalog_materials | db.material | Textbooks/syllabi/worksheets at any level |
Assignment | catalog_assignments | db.assignment | Reusable homework/project templates |
Book | catalog_books | db.book | Library books (≠ school SchoolBook) |
Quiz | catalog_quizzes | db.quiz | Defined; not seeded |
Document | catalog_documents | db.document | Unified file record (PDF/PPTX/DOCX/audio) |
Image | catalog_images | db.image | Concept-image variants (see Concepts) |
Textbook | catalog_textbooks | db.textbook | Textbook metadata (PDF S3 key on Subject.pdf) |
Curriculum | catalog_curricula | db.curriculum | The 12 curriculum records; curriculumId FK target |
Bridge models (carry schoolId)
| Model | Table (@@map) | Accessor | Purpose |
|---|---|---|---|
SubjectSelection | school_subject_selections | db.subjectSelection | One row per adopted subject+grade; isActive toggles a subject on/off |
ContentOverride | school_content_overrides | db.contentOverride | Per-school chapter/lesson/video override; isHidden hides content, hideQuiz hides a lesson's practice quiz |
InstructorPreference | school_instructor_preferences | db.instructorPreference | Per-school teacher/video ranking |
BookSelection | school_book_selections | db.bookSelection | Bridges a catalog Book into a school library |
School mirrors (catalog FK → catalogXxxId)
A catalog model and its school-scoped counterpart are different models. When
you read Exam, check the table to know which one. The school mirror links back to
the catalog through optional catalogSubjectId / catalogChapterId /
catalogLessonId FKs.
| Catalog (global) | School-scoped (per-tenant) |
|---|---|
Exam → catalog_exams | SchoolExam → exams |
Book → catalog_books | SchoolBook → books |
Assignment → catalog_assignments | SchoolAssignment → assignments |
Question → catalog_questions | QuestionBank → question_bank |
ExamTemplate → catalog_exam_templates | SchoolExamTemplate → exam_templates |
// QuestionBank, SchoolExam — link to catalog hierarchy
catalogSubjectId String?
catalogChapterId String?
catalogLessonId String?
// SchoolAssignment — link to catalog lesson
catalogLessonId String?Enums
ContentStatus DRAFT, REVIEW, PUBLISHED, ARCHIVED, DEPRECATED
ApprovalStatus PENDING, APPROVED, REJECTED
ContentVisibility PRIVATE, SCHOOL, PUBLIC, PAID
SchoolLevel ELEMENTARY, MIDDLE, HIGH
StreamType GENERAL, SCIENCE, ARTS, COMMERCE, TECHNICAL, RELIGIOUS
MaterialType TEXTBOOK, SYLLABUS, REFERENCE, STUDY_GUIDE, PROJECT, WORKSHEET,
PRESENTATION, LESSON_NOTES, VIDEO_GUIDE, LAB_MANUAL, OTHER
QuestionType MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER, ESSAY, FILL_BLANK,
MATCHING, ORDERING, MULTI_SELECT
QuestionSource MANUAL, AI, IMPORTED
DifficultyLevel EASY, MEDIUM, HARD
BloomLevel REMEMBER, UNDERSTAND, APPLY, ANALYZE, EVALUATE, CREATE
ProposalType SUBJECT, CHAPTER, LESSON, VIDEO
ProposalStatus DRAFT, SUBMITTED, IN_REVIEW, APPROVED, REJECTED, PUBLISHED
EnrollmentStatus PENDING, ACTIVE, COMPLETED, CANCELLED, EXPIRED
ContentVisibility is the gate every contributed item carries: PRIVATE (only
the creator's school), SCHOOL (schools using that subject, free), PUBLIC
(anyone, free), PAID (anyone, gated by purchase). See
Visibility & Paid Content.
Content-model detail
Material (table catalog_materials) — books, syllabi, worksheets at any level:
| Field | Type | Notes |
|---|---|---|
catalogSubjectId / catalogChapterId / catalogLessonId | String? | One of these is set (FK into the catalog hierarchy) |
title / lang | String | lang defaults "ar" |
type | MaterialType | TEXTBOOK, SYLLABUS, WORKSHEET, … |
fileUrl / fileKey / fileSize / mimeType / pageCount | mixed | S3-backed |
externalUrl | String? | For non-uploaded resources |
contributedBy / contributedSchoolId | String? | Attribution |
approvalStatus / visibility | enums | Review + gating |
Assignment (table catalog_assignments) — same shape as Material plus
instructions/rubric (String?), totalPoints (Decimal?), estimatedTime
(Int?, minutes), assignmentType (homework/project/lab/essay/presentation).
ExamQuestion (table catalog_exam_questions) — junction linking Exam ↔
Question with order and a per-exam points override. Indexes:
@@unique([catalogExamId, order]), @@unique([catalogExamId, catalogQuestionId]).
Replaces the legacy Exam.questions JSON field.
Video, Attachment — full schemas in prisma/models/video.prisma
(lesson_videos, lesson_attachments). Key Video fields: videoUrl,
provider (youtube/vimeo/self-hosted), visibility, price,
approvalStatus, plus engagement (viewCount, likeCount, averageRating,
isFeatured). Paid videos are gated through VideoPurchase.
Provisioning
A school never copies the catalog. When onboarding finishes, completeOnboarding
(src/components/onboarding/legal/actions.ts) calls setupCatalogForSchool
(src/components/catalog/setup.ts): it infers the curriculum, finds the
matching subjects through a progressive fallback, builds the academic
levels/grades/streams from the per-curriculum config, and writes one
SubjectSelection bridge row per adopted subject and grade.
import { setupCatalogForSchool } from "@/components/catalog/setup"
await setupCatalogForSchool(schoolId, { country: "US", curriculum: "US" })
await setupCatalogForSchool(schoolId, { country: "AE", curriculum: "IB-DP" })
await setupCatalogForSchool(schoolId, { country: "GB", curriculum: "GB" })inferCurriculum + findSubjects fallback
A school row stores no curriculum code — inferCurriculum() derives it from
country + type at runtime. findSubjects() then resolves subjects through a
4-step progressive fallback, so a school always gets something:
| Step | Filter |
|---|---|
| 1 | country + curriculum + schoolType |
| 2 | country + curriculum |
| 3 | country="*" + curriculum (worldwide) |
| 4 | US (baseline fallback) |
Grade-specific subjects produce a 1:1 mapping — "Math Grade 3" maps directly to
the school's Grade 3. Knowing the grade + curriculum means knowing the subjects —
subjects are bound to the grade at bridge time, so a section's grade already implies
its subjects. The classrooms block is lean (grades → sections → one main classroom
each) and never generates classes from catalog; Class records are the
per-subject gradebook axis, not something provisioned from the Classrooms Configure
page. See Classrooms › Integration.
ensureSubjectSelections — lazy self-heal
Provisioning runs fire-and-forget via Next.js after(). If bridge rows go
missing, ensureSubjectSelections repairs them lazily on the first read, so a
partial provisioning failure self-corrects for subject selections.
The provisioning doctor — repairProvisioning
Selections were the only stage that self-healed; a serverless timeout could
still leave a school without periods, sections, or timetable slots. The doctor
in src/components/catalog/provision.ts closes that gap:
getProvisioningStatus(schoolId)— one batched round of counts across every stage (defaults, academic structure, selections, schedule, sections, timetable slots, join code) plus report-only extras (streams, library books).repairProvisioning(schoolId)— runs only the missing stages, in dependency order, each one isolated so a single failure never blocks the rest. Every underlying step is idempotent — re-running on a healthy school is a no-op.
Three callers converge on it: onboarding's after() hook, the manual
publishSchool fallback, and the operator tenants table (Repair
Provisioning row action). applyTimetableStructureForNewSchool is also
idempotent now — a re-run no longer duplicates the school's periods.
Per-curriculum academic structure (the fixed gap)
The old setup.ts hardcoded the Sudanese model — Arabic 6+3+3 levels, Arabic
grade names, forced Science/Arts streams on grades 10–12 — and applied it to
every school, so a US/UK/IB school got Arabic grade names and streams it
shouldn't have. This is now fixed. Provisioning resolves the school's
curriculum code to a CurriculumAcademicConfig in
src/components/catalog/academic-config.ts (CURRICULUM_ACADEMIC_CONFIG,
selected via getAcademicConfig). The SD entry preserves the original constants
byte-for-byte, so Sudanese schools provision exactly as before.
| Curriculum code(s) | Structure | Grade naming | Streams |
|---|---|---|---|
SD, SA, EG, AE, QA, KW, JO | Arabic 6+3+3 | الصف الأول … | Science / Arts from grade 10 |
US, CAIE-IGCSE, IB-DP | English 5+3+4 | Grade N | None |
GB | Key stages (6+3+3 map) | Year N | None |
CBSE | 5+3+4 (primary/middle/secondary) | Class N | None |
| (unknown) | Generic English 6+3+3 | Grade N | None (GENERIC_FALLBACK) |
Grade slugs are intentionally not configurable — they stay
grade-${n}everywhere because theYearLevelmapping insetup.tsmatches on that slug shape. Only the display name, level boundaries, and lumos policy vary per curriculum.
Proposals & Approval
A school can propose new catalog content and, on approval, opt in to it. This is the request flow shipped this session — note the approval publishes to the catalog only; there is no auto-bridge (the school adds it explicitly).
School proposes a subject/chapter/lesson
→ Proposal { status: SUBMITTED } (requireProposer: ADMIN / DEVELOPER / TEACHER)
Operator queue at /catalog/proposals (sidebar badge shows SUBMITTED count)
→ approveProposal → creates Subject/Chapter/Lesson (status PUBLISHED) in the catalog
→ proposal PUBLISHED, catalogEntityId set
→ NO auto-bridge (opt-in)
→ notifies proposer + school ADMINs (in_app + email, school-language aware)
School catalog picker pins the new subject ("Requested by your school — now ready")
→ one-time reminder dialog + Add CTA
→ pin clears once the subject is added (SubjectSelection exists)
→ rejectProposal → notifies proposer + ADMINs with the rejection reason
Proposing (school side)
submitSubjectProposal / submitChapterProposal / submitLessonProposal
(src/components/school-dashboard/listings/subjects/catalog/proposal-actions.ts)
write a Proposal row (catalog_proposals) with status: SUBMITTED. Authorized
by requireProposer — ADMIN, DEVELOPER, or TEACHER with a schoolId. The
Proposal.data JSON carries the entity fields; chapter/lesson proposals reference
parentSubjectId / parentChapterId. A DRAFT or REJECTED proposal can be edited
and resubmitted (updateProposal).
Operator queue
/catalog/proposals (DEVELOPER only) lists proposals via getProposalsForReview,
defaulting the filter to SUBMITTED. The catalog layout computes badge counts —
db.proposal.count({ where: { status: "SUBMITTED" } }) — and renders the nav
label as Proposals (N) when pending.
Approval → catalog only
approveProposal runs the whole flow in one transaction (optimistic status check
inside the tx, slug-collision retry, Arabic names get a random slug suffix). It
creates the Subject/Chapter/Lesson with status: PUBLISHED, marks the
proposal PUBLISHED with catalogEntityId, and does not create a
SubjectSelection — the school opts in. Notifications fire after the
transaction (a notification failure never rolls back the approval):
- School language resolved via
resolveSchoolLang(schoolId)(bilingual title/body). - Reuses
NotificationTypedocument_sharedfor approval,system_alertfor rejection. metadatacarries{ entityType: "proposal", kind: "proposal_approved" | "proposal_rejected", catalogEntityId, url: "/subjects/catalog" }.- Delivered to the proposer (skipped if the proposer is an ADMIN — the
role-audience dispatch already covers them, avoiding duplicates) and all
school ADMINs, on
in_app+email. rejectProposalrequires a reason and includes it in the notification.
Pinned in the school picker
The school catalog picker (subject-picker.tsx) takes pinnedSubjectIds —
approved subjects this school requested but hasn't added yet. They render in a
pinned "Requested by your school — now ready" section with an Add CTA, and
a one-time CatalogReadyReminder dialog announces them (dismissal stored in
localStorage under catalog-ready-dismissed:${schoolId}). The pin filters out
any subject that already has a selection, so it clears automatically once the
subject is added.
Visibility & Paid Content
Every contributed item carries a ContentVisibility plus optional
price/currency. The contribution lifecycle:
Teacher/School creates content
→ approvalStatus: PENDING, visibility: PRIVATE, status: DRAFT
SaaS admin reviews (/catalog/approvals)
→ APPROVED → status set to PUBLISHED, visibility set by reviewer
PRIVATE — only creator's school
SCHOOL — schools using that subject (free)
PUBLIC — anyone (free)
PAID — anyone, gated by purchase (priceable types only)
→ REJECTED → reason sent to creator
Approval now publishes (the fixed gap)
Approved school contributions are created with status: DRAFT, but school browse
paths only surface PUBLISHED + APPROVED rows — so previously an approved
contribution stayed DRAFT-invisible. approveContent
(saas-dashboard/catalog/approval-actions.ts) now also sets
status: PUBLISHED for Question, Material, and Assignment (Video has no
ContentStatus; Book is published + auto-selected into the contributor's library).
Paid videos (wired)
Videos are fully Stripe-gated: a VideoPurchase row records a purchase, the URL is
served through signed CloudFront, and an unpurchased PAID video returns a
null URL so the player can't fetch it. Setting a video PAID at approval
requires a positive price and a 3-letter currency.
Paid exams / questions (deferred)
Question and Exam carry price/currency and accept PAID visibility, but
the purchase flow is not built — the fields exist for a future paid
exam/question store.
Operator flag management
updateContentFlags (+ the content-flags-dialog) lets a DEVELOPER set
visibility / status / price per content type, independent of approve/reject. The
dialog enforces a per-type whitelist:
| Field | Allowed on |
|---|---|
visibility | every type |
status | every type except Video (no ContentStatus) |
price / currency | Question, Video, Exam only |
PAID | priceable types only — requires price + 3-letter currency |
School-side controls
SubjectSelection.isActive— turns an adopted subject on/off for the school.ContentOverride.isHidden— hides a chapter/lesson/video. Enforced on both school surfaces: the lumos LMS (get-course.tsdrops hidden nodes;admin-get-course.tstags everything with_isHiddeninstead) and the subject detail page (subjects/[slug]/page.tsxfilters chapters/lessons and the video rail for non-admin roles, while ADMIN/DEVELOPER see everything plus the mounted Customize panel —SchoolCatalogCustomization). The Customize panel (topic-overrides.tsx) exposes hide toggles for each chapter, lesson, and specific instructor video (the per-video toggle now receives the lesson's videos + their override state — previously the data was never passed, so it never rendered).ContentOverride.hideQuiz— hides a lesson's practice quiz for the school while keeping the lesson visible (set viasetLessonQuizHidden; a lesson override row can carryisHiddenand/orhideQuiz). Enforced in lumos'sget-lesson-content.ts(returns no questions when hidden).InstructorPreference— the admin's preferred video source per subject (platform / school / a specific teacher);get-lesson-with-progress.tsranks the lesson's videos by it. Video visibility itself is owner-controlled (PRIVATE owner-only, SCHOOL, PUBLIC, PAID);removeVideoPaywallconverts a PAID video back to a free audience.- Global search now filters to PUBLISHED-only subjects
(
atom/generic-command-menu/server/global-search.ts) — a prior leak of unpublished content is fixed.
Curriculum Engineering
The catalog hierarchy is three global tables, all scope-free; schools reference
them via the bridge. slug is the primary upsert key — it encodes country +
grade + subject (sd-g5-math, us-g5-math), so no compound unique on
(country, curriculum, grade, name) is needed.
model Subject { // table catalog_subjects
country String @default("SD") // ISO 3166-1 alpha-2
curriculum String @default("SD") // canonical code: bare ISO (US, SD, GB) or BODY-PROGRAMME (IB-DP, CAIE-IGCSE, CBSE)
curriculumId String? // optional FK → Curriculum
concept String? // shared image key
thumbnail String? @map("thumbnailKey") // S3 key for WebP variants
grades Int[] @default([]) // [5] = grade 5 only
levels SchoolLevel[] // [ELEMENTARY]
slug String @unique // sd-g5-math, us-g5-math
totalChapters Int // denormalized counts
totalLessons Int
}The catalog repository — databayt/catalog
The deep curricula no longer live in this repo. Their source of truth is
github.com/databayt/catalog, cloned
beside this repo as ../catalog, and it mirrors the CDN exactly:
../catalog/sd/g6/math/structure.json == cdn.databayt.org/catalog/sd/g6/math/structure.json
../catalog/sd/g6/math/c1/l3/qbank.json == cdn.databayt.org/catalog/sd/g6/math/c1/l3/qbank.json
<curriculum>/<grade>/<subject>/
structure.json # id, title {ar,en}, source, chapters c<N> → lessons l<N>
qbank.json · exams.json # subject level
c<N>/qbank.json · exams.json # chapter N (book order)
c<N>/l<N>/… # lesson N within that chapter
textbook.pdf · textbook.md · cover/thumbnail/banner.jpg · pages/<N>.webp
The subject id is <curriculum>-<grade>-<subject> (sd-g12-biology) — the
same three segments as the path, so there is no folder↔slug table. A question
lives once, in the folder of its scope; exams carry hogwarts' examType
vocabulary (final, midterm, chapter_test, practice, quiz,
diagnostic). Content changes go to the catalog repo (validated there by
pnpm validate), never to this one.
tree.ts — the one catalog seed
prisma/seeds/catalog/tree.ts (seedCatalogTree) reads the clone for sd,
gb, cbse, ib-dp and caie-igcse (the catalog's us tree is the Aldar
variant and is not adopted — us.ts keeps US K-12). Per subject it upserts
Subject, then adopts chapters and lessons instead of rebuilding them:
- a subject is found by its catalog id, else by its pre-catalog slug (the
catalog's
scripts/migrate/renames.json) and renamed in place — same id, sameSubjectSelections;proxy.ts308s old slugs (src/components/catalog/legacy-slugs.ts); - chapters/lessons are matched by their pre-catalog slug (or current
c<N>/l<N>) and renamed to their position — ids survive, so live-class sessions and lesson progress keep their lesson; - rows the catalog no longer has are set
ARCHIVEDwith anarchived-slug, never deleted.
Asset keys point at the canonical catalog/<cur>/<grade>/<subject>/… path
whenever the catalog's assets.lock.json has the file. Questions and exams are
rebuilt per subject from the files, scope from the folder (chapter-level
questions are spread over that chapter's lessons for the lesson practice quiz).
Without a clone the seed does nothing and deletes nothing, so deploy-time seeds
are safe. CATALOG_DIR / CATALOG_CURRICULA / CATALOG_CONCURRENCY tune it.
Curricula & codes
National curricula use a bare ISO 3166-1 country code — the country is
the identifier; international programmes use a {body}-{programme} code.
Existing rows were migrated from the legacy national / us-k12 / ib codes by
the (now-live) canonicalize_curriculum_codes migration. Curriculum.slug keeps
its own values (sd-national, ib-diploma).
| Curriculum | Code | Type |
|---|---|---|
| Sudan | SD | National |
| Saudi Arabia | SA | National |
| Egypt | EG | National |
| United States | US | National |
| England | GB | National |
| UAE | AE | National |
| Qatar | QA | National |
| Kuwait | KW | National |
| Jordan | JO | National |
| IB Diploma | IB-DP | International |
| Cambridge IGCSE | CAIE-IGCSE | International |
| Indian CBSE | CBSE | International |
Coverage
All twelve curricula have subjects. Six are deep (real chapters and
lessons) — US, Sudan, England (catalog gb), Cambridge IGCSE, IB-DP, and
Indian CBSE. The six Arab national curricula stay subjects-only until their
ministry textbook structures are authored.
| Curriculum | Code | Grades | Subjects | Chapters | Lessons | Source | Seed |
|---|---|---|---|---|---|---|---|
| United States | US | 1–12 | ~240 | ~750 | ~3,993 | US-inventory | us |
| Sudan | SD | 1–12 | 135 | 812 | 4,144 | catalog sd | sd |
| England | GB | 1–12 | 184 | 363 | 730 | catalog gb | gb |
| Cambridge IGCSE | CAIE-IGCSE | 10 | 8 | 76 | 76 | catalog caie-igcse | caie-igcse |
| IB Diploma | IB-DP | 12 | 9 | 49 | 49 | catalog ib-dp | ib |
| Indian CBSE | CBSE | 10 | 7 | 58 | 58 | catalog cbse | cbse |
| Saudi Arabia | SA | 1–12 | ~132 | none | none | inline sa.ts | sa |
| Egypt | EG | 1–12 | ~140 | none | none | inline eg.ts | eg |
| UAE | AE | 1–12 | ~144 | none | none | inline ae.ts | ae |
| Qatar | QA | 1–12 | ~122 | none | none | inline qa.ts | qa |
| Kuwait | KW | 1–12 | ~139 | none | none | inline kw.ts | kw |
| Jordan | JO | 1–12 | ~150 | none | none | inline jo.ts | jo |
The deep transnational curricula (Cambridge, IB, CBSE) cover a representative grade so far; extending to every grade is mechanical (author more
structure.jsonin the catalog repo). The catalog'sustree (Aldar American, 128 subjects) is not ingested — it collides on theus-g{N}-*slug space; it stays a reference overlay.
Textbook reader — the book as pages (2026-09-07)
/{lang}/subjects/{slug}/textbook opens the textbook the way the iOS Books app
opens a book: the cover first (a hero tinted from the cover, title, edition,
page/unit/lesson counts, Contents and Start reading, an About the
book sheet), then a designed contents page carrying the book's printed page
numbers, then the text one screen per page. Each chapter is a CSS multi-column
flow whose column is the viewport, so the browser's columns are the pages;
pages slide, edge taps, swipes and arrow keys turn them (mirrored for Arabic),
the centre tap hides the chrome, and the counter reads N of T (just N with
the chrome hidden). The round menu opens the reading menu — Contents — %,
Search Book (Arabic-folded, snippets with the printed page, the hit
highlighted in place), Themes & Settings (original / paper / quiet / night,
Thmanyah or Rubik, six sizes, three leadings), share, PDF, original pages
(the pages/<N>.webp scans, one per screen) and bookmarks. Printed numbers
rest on the printed→PDF offset, taken from the first source that has one: the
author's pageOffset in structure.json, the folios OCR left on the pages
(physics: PDF − 8), or a vote from the structure's own headings found in the
text (biology's folios are OCR debris; its headings vote 8). Chapter and
lesson pages come from the authoring structure.json published beside the
twin (catalog/textbooks/<slug>/structure.json — physics and biology so far;
other books fall back to name matching). A scan's first page is the cover
screen, not a page of text. Position, theme and text settings persist in the
browser. Files:
school-dashboard/listings/subjects/textbook/ (spine.ts, engine.ts,
book.tsx, sheets.tsx, cover.tsx, toc.tsx beside parse.ts and
article.tsx); tests in src/tests/school-dashboard/listings/subjects/.
Real content — Sudan (all grades)
The Sudan question banks and exams come with the tree seed: 7,395 unique
questions and 1,533 exams across g1–g12, each question stored in its
chapter/lesson folder in the catalog (the id-parsing rules that used to live in
sd-content.ts placed them once, in the catalog migration). 3,754
template-generated placeholders were removed there. Rows are tagged
["sd", "<grade>", "<subject>"]; content.ts skips synthetic content for any
subject with "sd"-tagged questions.
Sudan lessons deliberately have no Video rows — the lumos lesson player
falls back to the shared story.mp4 clip on every SD lesson, so the player
surface is never empty and never points at a missing CDN object.
Generated content
catalog/content.ts reads the published hierarchy and generates synthetic
rows in bulk: exams (per subject/chapter/lesson), a question bank (~10/subject,
6/chapter, 4/lesson), materials (metadata only — fileUrl is null), and one
assignment per lesson. A subjects-only curriculum gets only per-subject rows.
Subjects that already carry real ingested content (questions tagged "sd")
are skipped, and orphaned synthetic rows (scope FKs nulled by chapter rebuilds)
are pruned on every run. The exception is catalog/exam-templates.ts, which
writes real hand-authored blueprints + templates (idempotent, every subject).
The Quiz model is not seeded. content.ts is idempotent — see
Operations.
TOC extraction quality
Many Sudan PDFs (especially grades 6–8 and 10–12) produced garbled or empty
pdftotext output, leaving chapters with 0 lessons:
| Grade range | With lessons | With 0 lessons |
|---|---|---|
| 1–5 | 31 (89%) | 4 (11%) |
| 6–9 | 8 (40%) | 12 (60%) |
| 10–12 | 14 (29%) | 34 (71%) |
Priorities: an OCR pass (Tesseract Arabic) on scanned PDFs, then manual entry for 10–12 via the content layer / proposal flow.
File map
../catalog/ # github.com/databayt/catalog — the content (mirrors the CDN)
prisma/
models/catalog.prisma # Subject, Chapter, Lesson + shared enums
seeds/catalog/
tree.ts # the catalog seed: sd · gb · cbse · ib-dp · caie-igcse
engine.ts # seedSubjectsOnly (shallow nationals)
registry.ts # 12 Curriculum records + curriculumId backfill
index.ts # seedCatalog + seedFullCatalog
us.ts # US (240, us-inventory.json)
{sa,eg,ae,qa,kw,jo}.ts # 6 subjects-only nationals
concepts.ts / banners.ts # shared image upload + backfill
content.ts / books.ts / videos.ts / exam-templates.ts / demo.ts
single.ts # CLI registry
src/components/catalog/
catalog-key.ts # CDN key builder (same as @databayt/catalog)
legacy-slugs.ts # pre-catalog subject slug → catalog id (proxy redirect)
setup.ts # findSubjects(), inferCurriculum()
academic-config.ts # per-curriculum CurriculumAcademicConfig
image.ts / image-url.ts # Sharp pipeline + URL resolution
concepts-data.ts # single source of concepts + nearestConcept
US Curriculum
The US source is a K-12 content inventory:
scripts/us-curriculum/us-inventory.json. 62 base subjects expand into ~240
grade-specific Subject records via the us seed. The US taxonomy and its
imagery seeded the shared concept assets and are the reference the
other curricula are optimized from.
| Metric | Count |
|---|---|
| Subjects | 62 |
| Topic groups | 201 |
| Sub-topics | 986 |
| Videos | 22,289 |
| Resources | 43,075 |
| Level | Subjects | Groups | Topics | Videos | Resources | Grades |
|---|---|---|---|---|---|---|
| Elementary | 18 | 49 | 346 | 8,130 | 17,233 | K-5 |
| Middle | 21 | 73 | 276 | 5,490 | 10,754 | 6-8 |
| High | 23 | 79 | 364 | 8,669 | 15,088 | 9-12 |
Each inventory sub-topic is keyed by slug:
type SourceTopic = {
slug: string // kebab-case identifier
name: string // display name
videos: number
resources: number
coverId: string
subject: string
topicGroup: string
}# list every sub-topic for a tier
jq '.tiers.elementary.subjects[] | .topics[]' scripts/us-curriculum/us-inventory.json
jq '.tiers.middle.subjects[] | .topics[]' scripts/us-curriculum/us-inventory.json
jq '.tiers.high.subjects[] | .topics[]' scripts/us-curriculum/us-inventory.jsonSudan Curriculum
Sudan covers grades 1–12 in a 6+3+3 structure (Elementary, Middle, Secondary),
extracted from textbook PDFs published by the National Center for Curricula and
Educational Research (Bakht Al-Rida). Subjects, chapters, and lessons live in
the catalog with country: "SD" and curriculum: "SD". Slug pattern
sd-g{N}-{subject} (e.g. sd-g5-math); the slug prefix is cosmetic, the
curriculum code is the canonical join key.
| Grade | Level | Subjects | Chapters | Lessons | Notes |
|---|---|---|---|---|---|
| 1 | Elementary | 4 | 17 | 173 | Arabic, English (SMILE Starter 1), Islamic, Math — 2020 series |
| 2 | Elementary | 4 | 22 | 131 | Same set, SMILE Starter 2 |
| 3 | Elementary | 6 | 36 | 176 | + Geography, Science (2020 series, updated 2026-09); English = SMILE Book 1 pupil's |
| 4 | Elementary | 8 | 40 | 232 | + Art, ICT, History, Science (added 2026-09); Math PDF added 2026-09 |
| 5 | Elementary | 14 | 66 | 406 | + ملبسنا، المورد، الأرض بيئة الحياة، home economics, technology |
| 6 | Elementary | 9 | 44 | 176 | + Art, Science (added 2026-09); English still the wrong reader (SMILE Book 4 not online) |
| 7 | Middle | 9 | 67 | 309 | Full set incl. SMILE Book 7 (2024) |
| 8 | Middle | 10 | 66 | 361 | Full set; Geography replaced + National Education added 2026-09; Art not online |
| 9 | Middle | 9 | 106 | 668 | 2024 finals, SMILE Book 9 |
| 10 | Secondary | 18 | 110 | 488 | New national curriculum (NCCER 2025) — see below |
| 11 | Secondary | 19 | 119 | 382 | Old curriculum, all streams (new editions expected 2026/27, none online yet) |
| 12 | Secondary | 25 | 192 | 870 | Shahada year — all streams; 13 structures rebuilt from the books' TOCs + 5 subjects added 2026-09 |
Grade 10 moved to the new Sudanese national curriculum in 2025/26 (NCCER first
editions dated 2025). Since 2026-09-04 the catalog's sd/g10/ carries that set: four
new subjects (التربية الوطنية، ريادة الأعمال، الفيزياء، and a unified اللغة العربية
book that replaces the old المطالعة/البلاغة/النحو sub-books), and eight subjects
replaced in place (biology, chemistry, history, maths 3rd ed., engineering,
التربية الإسلامية — formerly الدراسات الإسلامية, التربية الفنية — formerly الفنون
والتصميم, and تكنولوجيا المعلومات والاتصالات — formerly علوم الحاسوب). The old
editions were archived before the move to the catalog repo (they stay in the
local pre-catalog archive, not in the catalog). Their DB rows
(sd-g10-literature, sd-g10-rhetoric, sd-g10-arabic-advanced) remain
PUBLISHED until archived by decision. Structures come from the books' own tables
of contents (structure.json, source.basis: official-toc-2026-09); the ledger is
the catalog's docs/history/sd-textbook-audit.md.
Grade 11 followed on 2026-09-05: its 18 legacy structures (generic units, few
lessons) were rebuilt from the books' own tables of contents (19 subjects, 122
chapters, 511 lessons), and every grade-11 and grade-12 textbook now has a
Markdown twin beside the PDF on the CDN (catalog/sd/<grade>/<subject>/textbook.md,
produced by the textbook keyword: MarkItDown, an Arabic RTL repair, and a
tesseract OCR fallback, graded A–C in the front matter).
Secondary streams (10–12) are routed by patterns in setup.ts:
const SCIENCE_ONLY_PATTERNS = [
"physics",
"chemistry",
"فيزياء",
"كيمياء",
"أحياء",
]
const ARTS_ONLY_PATTERNS = [
"philosophy",
"فلسفة",
"أدب",
"بلاغة",
"مطالعة",
"نحو",
"عربية خاصة",
"رياضيات أساسية",
]Data pipeline
~/SD/*.pdf (260 raw, ~112 unique textbooks)
→ organize → scripts/sudan-data/pdfs/grade-{N}/sd-g{N}-{subject}.pdf
→ pdftotext → raw Arabic TOC text
→ parse TOC → scripts/sudan-data/toc/sd-g{N}-{subject}.json (97 files)
→ assemble-sudan-curriculum.ts → scripts/sudan-data/sudan-curriculum.json (master)
→ prisma/seeds/catalog/sd.ts → Subject + Chapter + Lesson (upsert)
Known gaps (2026-09-04): grade-3 English should be SMILE Starter 3 and grade-6 English SMILE Book 4 (neither is online), grade-8 Art is not online, the new-curriculum grade-10 books for Quran, French, family sciences, military sciences, English and Christian education have not been issued online, and the eleven subjects whose textbooks were replaced still carry their old-curriculum question banks at subject scope (re-authoring pending).
Adding a national curriculum
A new national curriculum (e.g. Egypt EG) is authored in the catalog repo,
then adopted here:
- In github.com/databayt/catalog: add
egtovocab/curricula.jsonand authoreg/curriculum.json+eg/g{N}/<subject>/structure.json(pnpm validate). - Add
eg: { country: "EG", code: "EG", sortBase: … }toCURRICULAinprisma/seeds/catalog/tree.ts(and asingle.tsentry if wanted). - Add
EG → "EG"toinferCurriculum()andCOUNTRY_CURRICULUM(src/lib/timetable-reference.ts). - Add Arabic names to
SUBJECT_CONCEPT_BY_NAMEinconcepts-data.ts. - (Optional) add an
EGentry toCURRICULUM_ACADEMIC_CONFIGif it differs from the shared Arabic 6+3+3 default. - Run
pnpm db:seed:single egthenpnpm db:seed:single concepts.
Concepts
A concept is a curriculum-agnostic identifier that maps every subject to
shared visual assets. Arabic (Sudan) and World Languages (US) share the same
languages concept and the same S3 images. The artwork was originally referenced
from an external library during prototyping; those assets are now owned,
redrawn, and shared across every curriculum — see Origins.
Subject (any curriculum)
└── concept: "math"
├── thumbnail: catalog/concepts/g{grade}-math/thumbnail-{size}.webp
├── banner: catalog/concepts/g{grade}-math/banner-{size}.webp
└── cover: catalog/concepts/math/cover
The 23 concepts
arts, biology, career-tech, celebrations, chemistry, civics,
computer-science, earth-science, economics, english, geography,
health, history, languages, life-skills, math, pe, physics,
psychology, religion, science, sociology, teacher-pd.
Source images live in public/subjects/concepts/{concept}.jpg. The concepts
seed processes each into 4 WebP variants (sm/md/lg/original) and uploads to S3 for
all 12 grades. All 12 curricula map their subjects to one of these 23 concepts;
the Image model (catalog_images) stores the per-variant records.
One source of truth — concepts-data.ts
Everything concept-related lives in src/components/catalog/concepts-data.ts: the
23 concepts and colors, the exact-match maps (SUBJECT_CONCEPT_BY_SLUG,
SUBJECT_CONCEPT_BY_NAME), the rotation pools (CONCEPT_POOL), and a
nearestConcept() keyword heuristic (EN + AR) so no subject is ever
image-less (a flat color is the last resort). Every seed (us.ts, sd.ts,
engine.ts, concepts.ts) imports from it — no per-seed copies remain.
Chapter rotation pools
Chapters within a subject rotate through a pool of 5 related concepts for visual
variety (cycled by sequenceOrder % poolSize): e.g. math → math, science,
computer-science, physics, economics; science → science, biology, chemistry,
physics, earth-science. The catalog seed (tree.ts) inherits this; for
subjects-only curricula, chapter/lesson images come for free once depth lands.
Variants
| Variant | Width | Quality | Use |
|---|---|---|---|
-sm.webp | 200px | 95 | Grid thumbnails |
-md.webp | 600px | 97 | Card images |
-lg.webp | 1200px | 100 | Hero banners |
-original.webp | Full | Lossless | Full resolution |
image.ts (server-only — processAndUploadCatalogImage, deleteCatalogImage)
runs Sharp; image-url.ts (client-safe — getCatalogImageUrl) builds
{key}-{size}.webp at read time. URL resolution: CloudFront from thumbnailKey →
concept-based static fallback → null (color background).
CDN & Assets
Catalog assets follow a strict key convention and are served from S3 via
CloudFront. The full platform CDN reference (bucket, asset() helper, the 5 flat
UI-asset directories) is at CDN & Assets — this section covers
only the catalog-specific asset story.
Subject carries four S3 key fields — thumbnail (@map("thumbnailKey")),
banner (@map("bannerUrl")), cover (textbook cover), pdf (textbook PDF).
The naming convention distinguishes a *Key (raw S3 key, variants appended at
read time) from a *Url (a fully-formed URL, e.g. external book covers). S3
layout:
catalog/concepts/g{n}-{concept}/thumbnail-{sm,md,lg,original}.webp
catalog/concepts/g{n}-{concept}/banner-{sm,md,lg,original}.webp
catalog/concepts/{concept}/cover
catalog/textbooks/{slug}/textbook.pdf
catalog/subjects/{slug}/…/thumbnail-{size}.webp # US lesson/chapter covers
Honest asset-gap table
| Asset | Produced by | Status |
|---|---|---|
| Subject thumbnails | concepts.ts | Complete — all 12 curricula, all 12 grades |
| Banners | banners.ts | Self-covering — ~10 of 23 concepts have their own legacy banner; the seed now borrows the nearest CONCEPT_POOL neighbor's for the rest (23/23 after next run) |
| Textbook covers | — | Missing — cover key set on every subject, object never uploaded (card falls back to icon) |
| Textbook PDFs | catalog assets push | Sudan + CBSE g10 science — pinned in the catalog's assets.lock.json |
| Lesson / chapter thumbnails | structure.json image | Sudan, all twelve grades — authored per chapter, lessons inherit (catalog docs/history/art-authoring/). US rows still render shared concept art; lesson-covers.ts is the older US path |
| Videos | videos.ts / contributed | Probe-verified — the seed HEAD-checks each .mp4 and only registers rows whose object exists (stale seed rows removed); real uploads still pending |
| Books | books.ts | External — 51 books on covers.openlibrary.org; scripts/snapshot-book-covers.ts localizes them to our CDN (run with prod AWS creds) |
| Materials | content.ts | Metadata-only — fileUrl null; material cards become clickable automatically once a fileUrl/externalUrl lands |
Operations
Seed commands
seedFullCatalog (the full seed) runs everything in canonical order — all 12
curricula → registry → concept images. The default catalog seed is the fast
subset: US + the full Sudan, registered. Every other curriculum is its own
explicit run. Always use pnpm db:seed:single <name> — never the full seed
(it is manual-only; see the rule below).
Seed (pnpm db:seed:single …) | Contents |
|---|---|
full | seedFullCatalog — all 12 curricula → registry → concept images (fresh DB) |
catalog | Fast default: US (~240) + full Sudan + registry |
us / sd | US (inventory) / Sudan from the catalog repo (tree.ts) |
gb / cbse / caie-igcse / ib / catalog-tree | Deep curricula from the catalog repo (tree.ts; catalog-tree = all) |
sa / eg / ae / qa / kw / jo | The 6 subjects-only nationals (seedSubjectsOnly) |
registry | All 12 Curriculum records + backfill Subject.curriculumId |
concepts / banners | Concept thumbnails & banners → Sharp → S3/CloudFront |
lesson-covers | US lesson/chapter thumbnails (CDN-dependent) |
books / content / videos / exam-templates | Books, generated content, videos, exam blueprints/templates |
demo | Academic structure + bridge rows for the demo school |
pnpm db:seed:single --list # list available seeds
pnpm db:seed:single catalog # fast default: US + full Sudan + registry
pnpm db:seed:single gb # British — deep, from the catalog repo
pnpm db:seed:single sa # Saudi Arabia — subjects-only national
pnpm db:seed:single registry # (re)link all 12 Curriculum records
pnpm db:seed:single concepts # thumbnails → S3/CDN
cd ../catalog && pnpm assets push --apply # textbook PDFs / art → CDN (catalog repo)Idempotency
Most seeds are upsert-safe (image seeds re-upload to immutable keys, also safe).
The content seed is now idempotent too (fixed this session): content.ts
computes a deterministic seedKey from each row's scope + title, pre-loads the
existing keys, and drops candidates that already exist — so re-running it no longer
duplicates exams/questions/materials/assignments. The Quiz model is still not
seeded.
Deploy-pending live-DB sync
Canonical codes are live, but the deep-curricula breadth is shipped in code and not yet in the live DB. The whole runbook is now one command:
pnpm tsx scripts/catalog-deploy-sync.ts # plan — read-only report
pnpm tsx scripts/catalog-deploy-sync.ts --execute # apply (deploy-gated)It inventories the stale subjects-only rows superseded by the deep trees
(gb-national-g* ~156, ib-diploma-g* ~114), deletes only the ones no
school references — SubjectSelection/Enrollment cascade on subject
delete, so referenced rows are listed for manual migration instead — then
seeds registry → gb → cbse → caie-igcse → ib → concepts → banners.
(Cambridge IGCSE and CBSE have no rows in the live DB yet.) Run it only
after the code deploy is live, with a Neon branch as backup. Two asset
follow-ups run in the same env: scripts/snapshot-book-covers.ts (OpenLibrary
covers → own S3) and scripts/snapshot-covers.ts (US lesson cover sources).
NEVER run the full seed
Per project rule, never run pnpm db:seed — the full seed is manual-only.
Always use pnpm db:seed:single <name>. Destructive DB operations (re-running
migration SQL, db push --accept-data-loss, migrate reset) are forbidden
without explicit approval.
Origins
The US-curriculum taxonomy (subjects → chapters → lessons) and the original concept artwork were inspired by ClickView, the American LMS first referenced. Those assets are now owned, redrawn, and shared across every curriculum, and the US catalog is the reference the others are optimized from — ClickView is credited here with thanks; it is no longer a runtime dependency.
See also
On This Page
OverviewStatus at a glanceArchitectureGlobal catalog modelsBridge models (carryschoolId)School mirrors (catalog FK → catalogXxxId)EnumsContent-model detailProvisioninginferCurriculum + findSubjects fallbackensureSubjectSelections — lazy self-healThe provisioning doctor — repairProvisioningPer-curriculum academic structure (the fixed gap)Proposals & ApprovalProposing (school side)Operator queueApproval → catalog onlyPinned in the school pickerVisibility & Paid ContentApproval now publishes (the fixed gap)Paid videos (wired)Paid exams / questions (deferred)Operator flag managementSchool-side controlsCurriculum EngineeringThe catalog repository — databayt/catalogtree.ts — the one catalog seedCurricula & codesCoverageTextbook reader — the book as pages (2026-09-07)Real content — Sudan (all grades)Generated contentTOC extraction qualityFile mapUS CurriculumSudan CurriculumData pipelineAdding a national curriculumConceptsThe 23 conceptsOne source of truth — concepts-data.tsChapter rotation poolsVariantsCDN & AssetsHonest asset-gap tableOperationsSeed commandsIdempotencyDeploy-pending live-DB syncNEVER run the full seedOriginsSee also