0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Help center
  • Common questions
  • 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

Catalog

PreviousNext

The platform-wide curriculum backbone — global content tables, schoolId-scoped bridges, provisioning, the proposal-and-approval flow, paid content, the curriculum engine, and per-curriculum coverage.

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 model
catalog.prisma— Subject — content root
chapter.prisma— Chapter
lesson.prisma— Lesson
curriculum.prisma— Curriculum
video.prisma— Video, VideoPurchase, Attachment
qbank.prisma— Question, ExamQuestion
exam.prisma— Exam, ExamTemplate
material.prisma— Material
assignment.prisma— Assignment
book.prisma— Book
textbook.prisma— Textbook
document.prisma— Document
proposal.prisma— Proposal
bridge.prisma— SubjectSelection, ContentOverride, InstructorPreference, BookSelection
enrollment.prisma— Enrollment, LessonProgress, SubjectCertificate
academic.prisma— AcademicLevel, AcademicGrade, AcademicStream
prisma/seeds/catalog/— load content into the DB
index.ts— orchestrator: seedCatalog (US + full SD) + seedFullCatalog
engine.ts— seedSubjectsOnly — the shallow nationals' shared seeder
registry.ts— all 12 Curriculum records + curriculumId backfill
us.ts— US — the US-curriculum source backbone
tree.ts— the catalog repo (../catalog) → sd · gb · cbse · ib-dp · caie-igcse: subjects, chapters, lessons, questions, exams — ids preserved
sa.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 S3
banners.ts— concept banners to S3 (owned, shared)
lesson-covers.ts— per-lesson/chapter covers to S3 (US, legacy)
books.ts— global catalog books
content.ts— materials, exams, questions
videos.ts— lesson videos
exam-templates.ts— exam blueprints + templates
demo.ts— demo-school structure + bridges
src/components/catalog/— shared core — every surface imports from here
concepts-data.ts— 23 concepts, colors, nearestConcept, exact-match maps + pools — single source
setup.ts— provisions a school onto the catalog
provision.ts— schedule/sections/timetable/library provisioning + the repair doctor
image.ts— Sharp pipeline + S3 upload
image-url.ts— resolve image URLs (CloudFront)
src/components/saas-dashboard/catalog/— platform authoring (DEVELOPER)
src/components/school-dashboard/listings/subjects/catalog/— school adoption + contribution
src/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 images
scripts/
us-curriculum/us-inventory.json— US source data
sudan-data/— Sudan source data + TOC + PDFs
  • Global catalog (catalog_* tables, no schoolId) — the curriculum hierarchy (Subject → Chapter → Lesson) plus the content attached to it (Exam, Question, Material, Assignment, Video, Book, Quiz, Document, Image, Textbook) and Curriculum records.
  • Bridge rows (school_* tables, carry schoolId) — 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.
ApproachData for 1,000 schools × ~240 subjectsSharing
Per-school clone220k subjects, 800k chapters, 4M lessonsNone
Shared catalog~240 subjects, ~800 chapters, ~4k lessons + bridge recordsEvery contribution enriches all schools

Status at a glance

CapabilityState
Global catalog + bridge modelWired — production
Curriculum breadth12 curricula wide, 6 deep (US, SD, GB, CAIE-IGCSE, IB-DP, CBSE)
Per-curriculum academic structureWired — 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 contentFixed — approval now sets status: PUBLISHED (was a DRAFT-invisible gap)
Paid videosWired — VideoPurchase, signed CloudFront, null-URL gating
Paid exams/questionsDeferred — fields exist, purchase flow not built
Subject thumbnailsComplete — all 12 curricula via shared concepts
Banners / textbook coversPartial / missing (see CDN & Assets)
Content seed idempotencyFixed 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

ModelTable (@@map)AccessorHolds
Subjectcatalog_subjectsdb.subjectRoot of the hierarchy; slug (unique), curriculum, concept, denormalized counts
Chaptercatalog_chaptersdb.chaptersubjectId FK, sequenceOrder
Lessoncatalog_lessonsdb.lessonchapterId FK, sequenceOrder, durationMinutes
Videolesson_videosdb.videovideoUrl, provider, visibility, price, approval
Examcatalog_examsdb.examCatalog exam (≠ school SchoolExam)
ExamQuestioncatalog_exam_questionsdb.examQuestionExam ↔ Question junction with order/points
Questioncatalog_questionsdb.questionCatalog question bank (≠ school QuestionBank)
Materialcatalog_materialsdb.materialTextbooks/syllabi/worksheets at any level
Assignmentcatalog_assignmentsdb.assignmentReusable homework/project templates
Bookcatalog_booksdb.bookLibrary books (≠ school SchoolBook)
Quizcatalog_quizzesdb.quizDefined; not seeded
Documentcatalog_documentsdb.documentUnified file record (PDF/PPTX/DOCX/audio)
Imagecatalog_imagesdb.imageConcept-image variants (see Concepts)
Textbookcatalog_textbooksdb.textbookTextbook metadata (PDF S3 key on Subject.pdf)
Curriculumcatalog_curriculadb.curriculumThe 12 curriculum records; curriculumId FK target

Bridge models (carry schoolId)

ModelTable (@@map)AccessorPurpose
SubjectSelectionschool_subject_selectionsdb.subjectSelectionOne row per adopted subject+grade; isActive toggles a subject on/off
ContentOverrideschool_content_overridesdb.contentOverridePer-school chapter/lesson/video override; isHidden hides content, hideQuiz hides a lesson's practice quiz
InstructorPreferenceschool_instructor_preferencesdb.instructorPreferencePer-school teacher/video ranking
BookSelectionschool_book_selectionsdb.bookSelectionBridges 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_examsSchoolExam → exams
Book → catalog_booksSchoolBook → books
Assignment → catalog_assignmentsSchoolAssignment → assignments
Question → catalog_questionsQuestionBank → question_bank
ExamTemplate → catalog_exam_templatesSchoolExamTemplate → 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:

FieldTypeNotes
catalogSubjectId / catalogChapterId / catalogLessonIdString?One of these is set (FK into the catalog hierarchy)
title / langStringlang defaults "ar"
typeMaterialTypeTEXTBOOK, SYLLABUS, WORKSHEET, …
fileUrl / fileKey / fileSize / mimeType / pageCountmixedS3-backed
externalUrlString?For non-uploaded resources
contributedBy / contributedSchoolIdString?Attribution
approvalStatus / visibilityenumsReview + 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:

StepFilter
1country + curriculum + schoolType
2country + curriculum
3country="*" + curriculum (worldwide)
4US (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)StructureGrade namingStreams
SD, SA, EG, AE, QA, KW, JOArabic 6+3+3الصف الأول …Science / Arts from grade 10
US, CAIE-IGCSE, IB-DPEnglish 5+3+4Grade NNone
GBKey stages (6+3+3 map)Year NNone
CBSE5+3+4 (primary/middle/secondary)Class NNone
(unknown)Generic English 6+3+3Grade NNone (GENERIC_FALLBACK)

Grade slugs are intentionally not configurable — they stay grade-${n} everywhere because the YearLevel mapping in setup.ts matches 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 NotificationType document_shared for approval, system_alert for rejection.
  • metadata carries { 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.
  • rejectProposal requires 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:

FieldAllowed on
visibilityevery type
statusevery type except Video (no ContentStatus)
price / currencyQuestion, Video, Exam only
PAIDpriceable 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.ts drops hidden nodes; admin-get-course.ts tags everything with _isHidden instead) and the subject detail page (subjects/[slug]/page.tsx filters 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 via setLessonQuizHidden; a lesson override row can carry isHidden and/or hideQuiz). Enforced in lumos's get-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.ts ranks the lesson's videos by it. Video visibility itself is owner-controlled (PRIVATE owner-only, SCHOOL, PUBLIC, PAID); removeVideoPaywall converts 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, same SubjectSelections; proxy.ts 308s 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 ARCHIVED with an archived- 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).

CurriculumCodeType
SudanSDNational
Saudi ArabiaSANational
EgyptEGNational
United StatesUSNational
EnglandGBNational
UAEAENational
QatarQANational
KuwaitKWNational
JordanJONational
IB DiplomaIB-DPInternational
Cambridge IGCSECAIE-IGCSEInternational
Indian CBSECBSEInternational

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.

CurriculumCodeGradesSubjectsChaptersLessonsSourceSeed
United StatesUS1–12~240~750~3,993US-inventoryus
SudanSD1–121358124,144catalog sdsd
EnglandGB1–12184363730catalog gbgb
Cambridge IGCSECAIE-IGCSE1087676catalog caie-igcsecaie-igcse
IB DiplomaIB-DP1294949catalog ib-dpib
Indian CBSECBSE1075858catalog cbsecbse
Saudi ArabiaSA1–12~132nonenoneinline sa.tssa
EgyptEG1–12~140nonenoneinline eg.tseg
UAEAE1–12~144nonenoneinline ae.tsae
QatarQA1–12~122nonenoneinline qa.tsqa
KuwaitKW1–12~139nonenoneinline kw.tskw
JordanJO1–12~150nonenoneinline jo.tsjo

The deep transnational curricula (Cambridge, IB, CBSE) cover a representative grade so far; extending to every grade is mechanical (author more structure.json in the catalog repo). The catalog's us tree (Aldar American, 128 subjects) is not ingested — it collides on the us-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 rangeWith lessonsWith 0 lessons
1–531 (89%)4 (11%)
6–98 (40%)12 (60%)
10–1214 (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.

MetricCount
Subjects62
Topic groups201
Sub-topics986
Videos22,289
Resources43,075
LevelSubjectsGroupsTopicsVideosResourcesGrades
Elementary18493468,13017,233K-5
Middle21732765,49010,7546-8
High23793648,66915,0889-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.json

Sudan 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.

GradeLevelSubjectsChaptersLessonsNotes
1Elementary417173Arabic, English (SMILE Starter 1), Islamic, Math — 2020 series
2Elementary422131Same set, SMILE Starter 2
3Elementary636176+ Geography, Science (2020 series, updated 2026-09); English = SMILE Book 1 pupil's
4Elementary840232+ Art, ICT, History, Science (added 2026-09); Math PDF added 2026-09
5Elementary1466406+ ملبسنا، المورد، الأرض بيئة الحياة، home economics, technology
6Elementary944176+ Art, Science (added 2026-09); English still the wrong reader (SMILE Book 4 not online)
7Middle967309Full set incl. SMILE Book 7 (2024)
8Middle1066361Full set; Geography replaced + National Education added 2026-09; Art not online
9Middle91066682024 finals, SMILE Book 9
10Secondary18110488New national curriculum (NCCER 2025) — see below
11Secondary19119382Old curriculum, all streams (new editions expected 2026/27, none online yet)
12Secondary25192870Shahada 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:

  1. In github.com/databayt/catalog: add eg to vocab/curricula.json and author eg/curriculum.json + eg/g{N}/<subject>/structure.json (pnpm validate).
  2. Add eg: { country: "EG", code: "EG", sortBase: … } to CURRICULA in prisma/seeds/catalog/tree.ts (and a single.ts entry if wanted).
  3. Add EG → "EG" to inferCurriculum() and COUNTRY_CURRICULUM (src/lib/timetable-reference.ts).
  4. Add Arabic names to SUBJECT_CONCEPT_BY_NAME in concepts-data.ts.
  5. (Optional) add an EG entry to CURRICULUM_ACADEMIC_CONFIG if it differs from the shared Arabic 6+3+3 default.
  6. Run pnpm db:seed:single eg then pnpm 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

VariantWidthQualityUse
-sm.webp200px95Grid thumbnails
-md.webp600px97Card images
-lg.webp1200px100Hero banners
-original.webpFullLosslessFull 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

AssetProduced byStatus
Subject thumbnailsconcepts.tsComplete — all 12 curricula, all 12 grades
Bannersbanners.tsSelf-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 PDFscatalog assets pushSudan + CBSE g10 science — pinned in the catalog's assets.lock.json
Lesson / chapter thumbnailsstructure.json imageSudan, 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
Videosvideos.ts / contributedProbe-verified — the seed HEAD-checks each .mp4 and only registers rows whose object exists (stale seed rows removed); real uploads still pending
Booksbooks.tsExternal — 51 books on covers.openlibrary.org; scripts/snapshot-book-covers.ts localizes them to our CDN (run with prod AWS creds)
Materialscontent.tsMetadata-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
fullseedFullCatalog — all 12 curricula → registry → concept images (fresh DB)
catalogFast default: US (~240) + full Sudan + registry
us / sdUS (inventory) / Sudan from the catalog repo (tree.ts)
gb / cbse / caie-igcse / ib / catalog-treeDeep curricula from the catalog repo (tree.ts; catalog-tree = all)
sa / eg / ae / qa / kw / joThe 6 subjects-only nationals (seedSubjectsOnly)
registryAll 12 Curriculum records + backfill Subject.curriculumId
concepts / bannersConcept thumbnails & banners → Sharp → S3/CloudFront
lesson-coversUS lesson/chapter thumbnails (CDN-dependent)
books / content / videos / exam-templatesBooks, generated content, videos, exam blueprints/templates
demoAcademic 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

  • CDN & Assets
  • Library
  • Multi-Tenancy
StudentsLibrary

On This Page

OverviewStatus at a glanceArchitectureGlobal catalog modelsBridge models (carry schoolId)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

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.