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