0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • 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
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Authorization

PreviousNext

Pure RBAC permission checks colocated with each feature.

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

CategoryShapeReference
CRUD authorizationPermission checks for standard CRUD on one entity (~12 files, 100–200 lines)listings/teachers/authorization.ts
Domain authorizationOwnership-based checks with JSDoc permission matrix (~3 files, 80–150 lines)lumos/authorization.ts

Rules

#Rule
1Name the file authorization.ts — not permissions.ts (5 legacy files)
2Four standard exports: check<Entity>Permission, assert<Entity>Permission, can<Action><Entity>, getAllowedActions
3Define an action union type (type TeacherAction = "create" | "read" | ...)
4Import AuthContext from a shared module — don't redefine in every file
5Permission order: DEVELOPER early exit → schoolId required → school scope → role matrix → default deny
6Pure functions only — no auth(), no getTenantContext(), no DB calls

Four standard exports

ExportSignaturePurpose
check<Entity>Permission(auth, action, entity?) => booleanFull permission check
assert<Entity>Permission(auth, action, entity?) => voidThrows on failure
can<Action><Entity>(role) => booleanConvenience 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

  • AuthContext duplicated in 15+ files — import from @/lib/auth-context.
  • session: any in getAuthContext — use Session from next-auth.
  • Mixed pure + async guards — keep auth() calls in actions.ts, not here.
  • Missing authorization.ts (~20+ features) — inline if (role !== "ADMIN") in actions instead.
  • permissions.ts naming (5 files) — rename to authorization.ts.

What belongs where

ContentInNot in
Pure permission checksauthorization.tsactions.ts
Action type unionauthorization.tstypes.ts
AuthContext type@/lib/auth-contextevery feature
auth() callsactions.tsauthorization.ts
getTenantContext()actions.tsauthorization.ts

See also

  • Pattern
  • Actions
  • Multi-Tenancy
QueriesValidation

On This Page

CategoriesRulesFour standard exportsCanonical example — CRUDDomain authorization with ownershipPermission check orderAnti-patternsWhat belongs whereSee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.