AqooN Multi-School ERP System Architecture
By A Clusy user · Published July 24, 2026
Complete Prisma schema and core library setup for a multi-tenant school management system with 27 database models, RBAC permissions, and Next.js dev server deployment.
- erp
- prisma
- nextjs
- database-design
- rbac
- school-management
Inside this notebook
## AqooN SMS — Prisma Schema Generator The **Prisma schema** was written directly to `prisma/schema.prisma` via `write_file` — see the saved file for the complete schema. ### Database Models (27 total) **Core (3):** Tenant, Branch, User **Academic (4):** AcademicYear, Term, Class, Subject **People (4):** Student, Teacher, Parent, Enrollment **Attendance (1):** Attendance **Timetable (1):** Timetable **Exams (2):** Exam, Mark **Finance (4):** FeeStructure, FeeAllocation, Invoice, Payment **HR (2):** Payroll, Leave **Library (2):** LibraryBook, LibraryIssue **Hostel (3):** HostelBlock, HostelRoom, HostelAllocation **Transport (3):** TransportRoute, TransportStop, TransportAllocation **Inventory (1):** InventoryItem **Notifications (1):** Notification **Audit (1):** AuditLog (in `audit` schema) ### Enums (12) UserRole, Gender, BloodGroup, AttendanceStatus, ExamType, PaymentStatus, FeeType, LeaveStatus, BookStatus, TransportVehicleType, InventoryCategory, MaritalStatus ### Key Features - Multi-tenant isolation via `Tenant` model - `multiSchema` preview feature with separate `audit` schema - All models map to snake_case table names via `@@map` - Audit logging with JSON metadata - Cascade deletes on tenant-scoped relations
# Fix ambiguous OTHER enum values in Prisma schema
with open('/home/user/aqoon-sms/prisma/schema.prisma', 'r') as f:
content = f.read()
# FeeType OTHER -> FEE_OTHER
content = content.replace(
'enum FeeType {\n TUITION\n ADMISSION\n EXAMINATION\n LIBRARY\n SPORTS\n TRANSPORT\n HOSTEL\n LABORATORY\n ACTIVITY\n OTHER\n}',
'enum FeeType {\n TUITION\n ADMISSION\n EXAMINATION\n LIBRARY\n SPORTS\n TRANSPORT\n HOSTEL\n LABORATORY\n ACTIVITY\n FEE_OTHER\n}'
)
# Also fix any remaining @default(OTHER) that should now point to something
# Only InventoryCategory @default(OTHER) should remain OTHER
# Actually since we changed FeeType, the OTHER in FeeType was renamed to FEE_OTHER
# So @default(OTHER) on InventoryCategory fields is still fine
with open('/home/user/aqoon-sms/prisma/schema.prisma', 'w') as f:
f.write(content)
print('Fixed. Running validate...')Fixed. Running validate...
## AqooN SMS — Core Library Setup The following core library files were written via `write_file`: | File | Purpose | |------|---------| | `.env.example` | Environment template for all required variables | | `src/lib/prisma.ts` | Prisma client singleton (hot-reload safe) | | `src/lib/supabase.ts` | Supabase client (browser + admin with service role) | | `src/lib/permissions.ts` | RBAC system, navigation, audit helper, pagination | ### Permission System **12 roles** with hierarchy: SUPER_ADMIN (100) → STUDENT (20) **22 module permissions** control access by role — e.g.: - `fees` → SUPER_ADMIN, SCHOOL_OWNER, PRINCIPAL, ADMIN, ACCOUNTANT - `results` → SUPER_ADMIN, PRINCIPAL, ADMIN, TEACHER, STUDENT, PARENT - `audit` → SUPER_ADMIN, SCHOOL_OWNER (most restricted) **23 navigation items** are filterable by module permission via `getNavigation(role)`. ### Utilities - `createAuditLog()` — writes to `audit` schema - `buildPagination()` — `skip/take/page/pageSize` from query params
import subprocess, time, sys, os
# Kill any existing server on port 3000
os.system("pkill -f 'next dev' 2>/dev/null || true")
time.sleep(1)
# Start Next.js dev server in background
proc = subprocess.Popen(
["npx", "next", "dev", "-p", "3000"],
cwd="/home/user/aqoon-sms",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
# Wait for the server to be ready
for attempt in range(30):
time.sleep(0.5)
…[1m[38;2;173;127;168m▲ Next.js 16.2.11[39m[22m (Turbopack) - Local: http://localhost:3000 - Network: http://169.254.0.21:3000 [32m[1m✓[22m[39m Ready in 361ms ✅ Dev server is running! Access the app at the URL shown above (port 3000) Dashboard: http://localhost:3000/dashboard Login: http://localhost:3000/login
import subprocess, time, sys, os, threading
CWD = "/home/user/aqoon-sms"
# Kill any lingering processes
os.system("pkill -f 'next dev' 2>/dev/null || true")
os.system("pkill -f localtunnel 2>/dev/null || true")
time.sleep(1)
# 1. Start Next.js dev server
dev = subprocess.Popen(
["npx", "next", "dev", "-p", "3000"],
cwd=CWD, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
print(f"🚀 Dev server PID: {dev.pid}")
# Wait for it to be ready
for i in range(60):
ret = subprocess.run(
…🚀 Dev server PID: 4368 ✅ Server ready! (HTTP 200) 🌍 Public URL: https://aqoon-sms.loca.lt ✅ Download link: https://aqoon-sms.loca.lt/aqoon-sms-full.zip
import subprocess, time, sys, os, threading
CWD = "/home/user/aqoon-sms"
# Kill any lingering processes
os.system("pkill -f 'next dev' 2>/dev/null || true")
os.system("pkill -f localtunnel 2>/dev/null || true")
time.sleep(1)
# 1. Start Next.js dev server
dev = subprocess.Popen(
["npx", "next", "dev", "-p", "3000"],
cwd=CWD,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
print(f"🚀 Dev server PID: {dev.pid}")
…