- 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
- 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
Sales & GTM
authorization.ts holds pure RBAC permission checks for a feature — no auth() calls, no database, no side effects. Functions receive context as parameters and return booleans (or throw). actions.ts calls these helpers; the file itself stays pure.
Categories
| Category | Shape | Reference |
|---|---|---|
| CRUD authorization | Permission checks for standard CRUD on one entity (~12 files, 100–200 lines) | listings/teachers/authorization.ts |
| Domain authorization | Ownership-based checks with JSDoc permission matrix (~3 files, 80–150 lines) | lumos/authorization.ts |
Rules
| # | Rule |
|---|---|
| 1 | Name the file authorization.ts — not permissions.ts (5 legacy files) |
| 2 | Four standard exports: check<Entity>Permission, assert<Entity>Permission, can<Action><Entity>, getAllowedActions |
| 3 | Define an action union type (type TeacherAction = "create" | "read" | ...) |
| 4 | Import AuthContext from a shared module — don't redefine in every file |
| 5 | Permission order: DEVELOPER early exit → schoolId required → school scope → role matrix → default deny |
| 6 | Pure functions only — no auth(), no getTenantContext(), no DB calls |
Four standard exports
| Export | Signature | Purpose |
|---|---|---|
check<Entity>Permission | (auth, action, entity?) => boolean | Full permission check |
assert<Entity>Permission | (auth, action, entity?) => void | Throws on failure |
can<Action><Entity> | (role) => boolean | Convenience helper for one action |
getAllowedActions | (role) => Action[] | UI helper — which buttons to show |
Canonical example — CRUD
/**
* Permission Rules:
* - DEVELOPER: full access across schools
* - ADMIN: full access within their school
* - TEACHER: read all in school, edit own profile
* - STAFF, ACCOUNTANT: read-only
*/
import { UserRole } from "@prisma/client"
export type TeacherAction =
| "create"
| "read"
| "update"
| "delete"
| "export"
| "bulk_action"
| "assign_class"
export interface AuthContext {
userId: string
role: UserRole
schoolId: string | null
}
export interface TeacherContext {
id?: string
schoolId?: string
userId?: string | null
}
export function checkTeacherPermission(
auth: AuthContext,
action: TeacherAction,
teacher?: TeacherContext
): boolean {
const { role, userId, schoolId } = auth
if (role === "DEVELOPER") return true
if (!schoolId) return false
if (role === "ADMIN") {
if (!teacher?.schoolId) return true
return schoolId === teacher.schoolId
}
if (role === "TEACHER") {
if (action === "read") return schoolId === teacher?.schoolId
if (action === "update")
return teacher?.userId === userId && schoolId === teacher.schoolId
return false
}
if (["STAFF", "ACCOUNTANT"].includes(role)) {
return action === "read" || action === "export"
}
return false
}
export function assertTeacherPermission(
auth: AuthContext,
action: TeacherAction,
teacher?: TeacherContext
): void {
if (!checkTeacherPermission(auth, action, teacher)) {
throw new Error(`Unauthorized: ${auth.role} cannot ${action} teacher`)
}
}
export function canCreateTeacher(role: UserRole): boolean {
return ["DEVELOPER", "ADMIN"].includes(role)
}
export function getAllowedActions(role: UserRole): TeacherAction[] {
switch (role) {
case "DEVELOPER":
case "ADMIN":
return [
"create",
"read",
"update",
"delete",
"export",
"bulk_action",
"assign_class",
]
case "TEACHER":
return ["read", "update"]
case "STAFF":
case "ACCOUNTANT":
return ["read", "export"]
default:
return []
}
}Domain authorization with ownership
For features with ownership semantics (lumos courses), use a JSDoc matrix table and an ownership check:
/**
* | Role | Create | Read | Update Own | Update Any | Delete Own | Enroll |
* |----------|--------|------|------------|------------|------------|--------|
* | DEVELOPER| Y | Y | Y | Y | Y | Y |
* | ADMIN | Y | Y | Y | Y | Y | Y |
* | TEACHER | Y | Y | Y | N | Y | Y |
* | STUDENT | N | Y | N | N | N | Y |
*/
if (role === "TEACHER") {
if (action === "create") return true
if (!course?.userId) return false
return course.userId === userId // ownership check
}Permission check order
1. DEVELOPER early exit → return true
2. schoolId required → return false if missing
3. School scope match → return false if entity.schoolId !== auth.schoolId
4. Role-specific matrix → ADMIN, TEACHER, STAFF, etc.
5. Default deny → return false
Anti-patterns
AuthContextduplicated in 15+ files — import from@/lib/auth-context.session: anyingetAuthContext— useSessionfromnext-auth.- Mixed pure + async guards — keep
auth()calls inactions.ts, not here. - Missing
authorization.ts(~20+ features) — inlineif (role !== "ADMIN")in actions instead. permissions.tsnaming (5 files) — rename toauthorization.ts.
What belongs where
| Content | In | Not in |
|---|---|---|
| Pure permission checks | authorization.ts | actions.ts |
| Action type union | authorization.ts | types.ts |
AuthContext type | @/lib/auth-context | every feature |
auth() calls | actions.ts | authorization.ts |
getTenantContext() | actions.ts | authorization.ts |