- Introduction
- Pitch
- Hogwarts
- Live Demo
- MVP
- Roadmap
- Launch Sprint
- PRD
- 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
- Playwright
- Prettier
- Block Rebound
The library uses a global-first architecture — every school sees the platform-wide CatalogBook catalog out of the box. No setup, no onboarding wiring. Schools can hide individual books; borrowing is tracked via lazily-provisioned school-scoped Book records.
Architecture
GLOBAL (no schoolId)
CatalogBook (PUBLISHED + APPROVED) ← all schools see by default
OPT-OUT (schoolId-scoped)
SchoolBookSelection
isActive: false → hidden for this school
no record → visible (default)
LAZY PROVISIONING
Book (school-scoped, auto-created on first detail page visit)
copied from CatalogBook, used for borrow / return tracking only
BorrowRecord (school-scoped)
userId, bookId, borrowDate, dueDate, status (BORROWED / RETURNED / OVERDUE)
| Approach | Setup | Data for 100 schools × 90 books | Sharing |
|---|---|---|---|
| Per-school provisioning | Admin adds | 9,000 Book records | None |
| Onboarding wiring | Auto | 9,000 Book records | None |
| Global-first (current) | Zero | 90 CatalogBooks (shared) | Visible to all |
Benefits: zero setup, no duplication, school autonomy (can hide), lazy efficiency, community contributions enrich all schools.
User flows
Student browses + borrows
Student visits /library
→ content.tsx queries CatalogBook directly (global)
excludes books hidden by school (SchoolBookSelection.isActive = false)
categorizes into rows: Latest, Featured, Literature, Science
→ click book card → /library/books/[id]
loads CatalogBook by ID
finds or creates school-scoped Book (lazy)
shows cover, description, video, rating, borrow status, "More by Author"
→ click "Borrow Book"
borrowBook() creates BorrowRecord + decrements availableCopies
→ click "Return Book"
returnBook() updates status to RETURNED + increments availableCopies
Admin manages library
/library/catalog → browse global catalog, select / deselect, adjust copies + shelf
/library/admin → dashboard
/library/admin/books → manage table (add from catalog, edit details, delete)
Teacher contributes
/library/contribute → contributeBook() creates CatalogBook (PENDING + DRAFT)
sets contributedBy + contributedSchoolId
SaaS /catalog/approvals → approve → APPROVED + PUBLISHED (visible to all)
reject → REJECTED + reason
Routes
| Route | Purpose | Auth |
|---|---|---|
/library | Library home (hero + book rows) | All roles |
/library/books | All books with search / filter | All roles |
/library/books/[id] | Book detail + borrow / return | All roles |
/library/catalog | Browse global catalog | ADMIN, DEVELOPER |
/library/admin | Admin dashboard | ADMIN, DEVELOPER |
/library/admin/books | Manage books table | ADMIN, DEVELOPER |
/library/admin/books/new | Add book from catalog | ADMIN, DEVELOPER |
/library/contribute | Submit book contribution | ADMIN, TEACHER, DEVELOPER |
/library/contributions | My contributions history | ADMIN, TEACHER, DEVELOPER |
/library/my-profile | Borrow history + stats | All roles |
Data flow
Library home query
// Get books hidden by this school
const hiddenSelections = await db.schoolBookSelection.findMany({
where: { schoolId, isActive: false },
select: { catalogBookId: true },
})
const hiddenBookIds = new Set(hiddenSelections.map((s) => s.catalogBookId))
// Query global catalog, excluding hidden
const catalogBooks = await db.catalogBook.findMany({
where: {
status: "PUBLISHED",
approvalStatus: "APPROVED",
visibility: { in: ["PUBLIC", "SCHOOL"] },
...(hiddenBookIds.size > 0
? { id: { notIn: Array.from(hiddenBookIds) } }
: {}),
},
orderBy: [{ rating: "desc" }, { createdAt: "desc" }],
})Lazy book provisioning
// Load CatalogBook (global)
const catalogBook = await db.catalogBook.findFirst({
where: { id: bookId, status: "PUBLISHED", approvalStatus: "APPROVED" },
})
// Find or create school Book (for borrowing)
let schoolBook = await db.book.findFirst({
where: { schoolId, catalogBookId: bookId },
})
if (!schoolBook) {
schoolBook = await db.book.create({
data: {
schoolId,
catalogBookId: catalogBook.id,
title: catalogBook.title,
author: catalogBook.author,
// … copies CatalogBook fields
totalCopies: 3,
availableCopies: 3,
},
})
}Browsing uses CatalogBook (zero school data). Borrowing uses school Book (created on demand). No bulk provisioning.
Hide mechanism
await db.schoolBookSelection.upsert({
where: { schoolId_catalogBookId: { schoolId, catalogBookId } },
create: { schoolId, catalogBookId, isActive: false },
update: { isActive: false },
})- No record → visible (default)
isActive: true→ explicitly selected (from catalog UI)isActive: false→ hidden for this school
RBAC
Authorization via checkLibraryPermission(role, action) in authorization.ts. Cross-school access always denied.
| Role | read | create | update | delete | borrow | return | manage |
|---|---|---|---|---|---|---|---|
| DEVELOPER | Y | Y | Y | Y | Y | Y | Y |
| ADMIN | Y | Y | Y | Y | Y | Y | Y |
| TEACHER | Y | — | — | — | Y | Y | — |
| STUDENT | Y | — | — | — | Y | Y | — |
| GUARDIAN | Y | — | — | — | Y | Y | — |
| STAFF | Y | — | — | — | — | — | — |
| ACCOUNTANT | Y | — | — | — | — | — | — |
| USER | — | — | — | — | — | — | — |
Server actions
Book CRUD (actions.ts)
| Action | Function | Notes |
|---|---|---|
| Create | createBook(params) | Requires catalogBookId — no standalone creation |
| Update | updateBook(params) | Partial updates, verifies school ownership |
| Delete | deleteBook(params) | Blocked when active borrows exist |
Borrow / return
| Action | Function | Notes |
|---|---|---|
| Borrow | borrowBook({ bookId, userId, schoolId }) | Transaction: create BorrowRecord + decrement copies |
| Return | returnBook({ borrowRecordId, schoolId }) | Transaction: update status + increment copies |
| Overdue | markOverdueBooks() | Batch: BORROWED past due date → OVERDUE |
Catalog selection (catalog/actions.ts)
| Action | Function | Notes |
|---|---|---|
| Select | selectCatalogBook(catalogBookId, copies) | Creates bridge + Book in transaction |
| Deselect | deselectCatalogBook(catalogBookId) | Removes bridge, nullifies Book.catalogBookId |
| Toggle | toggleBookSelection(selectionId) | Flips isActive (show / hide) |
| Update | updateBookSelection(selectionId, data) | Adjust copies, shelf location |
Validation schemas
| Schema | Key fields | Constraints |
|---|---|---|
bookSchema | title, author, genre, rating, coverUrl, coverColor | rating 0-5, hex coverColor, totalCopies ≥ 1 |
borrowBookSchema | bookId, userId, schoolId, dueDate | dueDate must be future |
returnBookSchema | borrowRecordId, schoolId | Both required |
updateBookSchema | bookId + partial book fields | At least one field required |
deleteBookSchema | bookId, schoolId | Both required |
Types
interface BookListItem {
id: string
title: string
author: string
genre: string
coverUrl: string
coverColor: string | null
rating: number
createdAt: Date
}
interface Book extends BookListItem {
description: string
totalCopies: number
availableCopies: number
summary: string
gradeLevel: string
schoolId: string
// optional: isbn, publisher, publicationYear, language, pageCount, videoUrl
}BookListItem works for both CatalogBook query results and Book records — used by BookList and BookCard.
Configuration
| Constant | Value | Purpose |
|---|---|---|
MAX_BORROW_DAYS | 14 | Default loan period |
MAX_BOOKS_PER_USER | 5 | Borrow limit per student |
BOOKS_PER_PAGE | 20 | Pagination size |
DEFAULT_COVER_COLOR | #000000 | Fallback for missing covers |
Tests
pnpm vitest run src/components/library/ --reporter=verbose # 160 tests
pnpm vitest run src/components/catalog/__tests__/setup.test.ts --reporter=verbose # 54 tests| Suite | Tests | Coverage |
|---|---|---|
__tests__/actions.test.ts | 39 | All 6 server actions (create, update, delete, borrow, return, overdue) |
__tests__/authorization.test.ts | 35 | 8 roles × 7 actions + cross-school + edge cases |
__tests__/validation.test.ts | 46 | All 5 Zod schemas |
catalog/__tests__/actions.test.ts | 33 | 4 catalog selection actions |
contribute/__tests__/actions.test.ts | 7 | Contribution submission |
Data layer (CatalogBook)
Books extend the universal catalog with CatalogBook. Like subjects, books live globally (no schoolId); schools reference them via SchoolBookSelection and a school-scoped Book provisioned lazily for borrow tracking.
CatalogBook carries the canonical metadata (title, author, genre, ISBN, cover, digital file, rating, usageCount) plus the contribution audit trail (contributedBy, contributedSchoolId, approvalStatus, approvedBy, rejectionReason). SchoolBookSelection carries per-school overrides (totalCopies, availableCopies, shelfLocation, customName, isActive). Book.catalogBookId is nullable so deselecting preserves BorrowRecord history.
Contribution flow (specific to CatalogBook)
Teacher / Admin → /library/contribute → contributeBook()
- auth + tenant context
- creates CatalogBook (PENDING + DRAFT)
- sets contributedBy + contributedSchoolId
SaaS → /catalog/approvals
- approveContent("CatalogBook", id) → APPROVED
- rejectContent("CatalogBook", id) → REJECTED + reason
→ visible to all schools (PUBLIC visibility) or just the originating school (SCHOOL visibility)
The contribution form lives at /library/contribute; the contributor's history is at /library/contributions for both ADMIN and TEACHER roles.
SaaS dashboard CRUD
Route /catalog/books. DEVELOPER only — global records, no schoolId. createCatalogBook, updateCatalogBook, deleteCatalogBook plus a detail view at /catalog/books/[bookId] showing the full metadata, cover, digital file info, contribution details, and the list of selecting schools.
Approval integration
CatalogBook is one of five content types in the unified approval system — alongside CatalogQuestion, CatalogMaterial, CatalogAssignment, and LessonVideo. All five share approveContent() and rejectContent() in approval-actions.ts, which switches on contentType. They use the same enums (ApprovalStatus, ContentVisibility, ContentStatus) and the same SaaS review surface at /catalog/approvals.
Seed
pnpm db:seed:single books90 entries across 8 genres (Arabic Literature, Islamic Studies, Sciences, Mathematics, Quran Sciences, History & Geography, Children's, English, Computer Science & Reference) — all APPROVED + PUBLISHED + PUBLIC, with picsum.photos placeholder covers. Idempotent.
See also
- Catalog — universal catalog architecture
- Multi-tenancy — tenant scoping
On This Page
ArchitectureUser flowsStudent browses + borrowsAdmin manages libraryTeacher contributesRoutesData flowLibrary home queryLazy book provisioningHide mechanismRBACServer actionsBook CRUD (actions.ts)Borrow / returnCatalog selection (catalog/actions.ts)Validation schemasTypesConfigurationTestsData layer (CatalogBook)Contribution flow (specific to CatalogBook)SaaS dashboard CRUDApproval integrationSeedSee also