Multi-Tenancy Architecture
Multi-Tenancy Architecture
SaaniCare implements a school-based multi-tenancy model where each school operates as an isolated tenant.
Tenant Model
School as Tenant
- Each School is a tenant
- All data is scoped to a school
- Users belong to specific schools
- Complete data isolation between schools
School District Support
- School Districts can manage multiple schools
- District-level administrators can oversee all schools in their district
- Schools can be part of a district or standalone
Data Isolation
Database Level
All tables include a schoolId field to ensure data isolation:
// Example schema
model Student {
id String @id @default(uuid())
schoolId String
name String
// ... other fields
school School @relation(fields: [schoolId], references: [id])
@@index([schoolId])
}
API Level
All API endpoints automatically filter by the user’s school:
// Middleware ensures school context
const schoolId = req.user.schoolId
// All queries are scoped
const students = await prisma.student.findMany({
where: { schoolId }
})
User-School Relationship
User Roles
Users have roles within their school:
- Superadmin: System-wide access (can access all schools)
- District Admin: District-wide access
- School Admin: Single school access
- Teacher: Classroom-level access
- Parent: Child-level access
School Assignment
- Users are assigned to one or more schools
- Users can switch between schools (if they have access)
- School context is maintained in JWT token
Tenant Context
Request Context
Every API request includes:
- User ID
- School ID (from user’s school assignment)
- Role within that school
Frontend Context
Frontend apps:
- Store current school in state
- Include school ID in API requests
- Display school-specific data only
Benefits
Data Security
- Complete isolation between schools
- No cross-tenant data leakage
- School-level access control
Scalability
- Can scale per school
- Database partitioning by school
- Independent school configurations
Flexibility
- Each school can have different settings
- Custom configurations per school
- School-specific features
Implementation Details
Database Schema
model School {
id String @id @default(uuid())
name String
districtId String?
// ... other fields
district SchoolDistrict? @relation(fields: [districtId], references: [id])
users User[]
students Student[]
// ... other relations
}
model User {
id String @id @default(uuid())
schoolId String
// ... other fields
school School @relation(fields: [schoolId], references: [id])
}
API Middleware
// Auth middleware extracts school context
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const user = req.user
req.schoolId = user.schoolId
next()
}
// All routes use school context
router.get('/students', requireAuth, async (req, res) => {
const students = await prisma.student.findMany({
where: { schoolId: req.schoolId }
})
res.json(students)
})
Future Enhancements
- Cross-school reporting (for districts)
- School-to-school data sharing (with permissions)
- Multi-school user management
- School-level feature flags