Skip to main content

Organization & Multi-Tenancy Implementation

Date: December 26, 2025 Status: In Progress

Overview

This document outlines the Organization and Multi-Tenancy implementation for StemBlock AI, enabling schools and districts to manage their own users, classes, and invitations within isolated organizational boundaries.

Design Decisions

DecisionChoiceRationale
ApproachHybrid - New Organization entity integrated with WorkspacesClean separation while leveraging existing workspace infrastructure
Role HierarchyORG_ADMIN as new UserRoleClear role separation; scoped to single organization
MembershipSingle organization per userSimpler boundaries, cleaner data isolation
MigrationDefault organization for existing dataZero-downtime migration, backward compatible

Data Model

Organization Entity

model Organization {
id String @id @default(uuid())
name String
slug String @unique
description String?
logoUrl String?
settings Json? // Features, branding, limits
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// Relations
users User[]
classes Class[]
workspaces Workspace[]
invitations Invitation[]
}

Updated Entities

User: Added organizationId foreign key Class: Added organizationId foreign key Workspace: Added organizationId (nullable for personal workspaces) Invitation: Added organizationId (nullable for self-registration)

Role Enum

enum UserRole {
STUDENT
PARENT
COACH
ORG_ADMIN // Organization administrator
ADMIN // Platform administrator
}

Permission Matrix

ActionSTUDENTPARENTCOACHORG_ADMINADMIN
View own organizationAll
Update org settings---OwnAll
Create organization----
Delete organization----
Manage org users---OwnAll
Manage org classes--OwnOwnAll
Create invitations--ClassOrgAll
View org statistics---OwnAll

API Endpoints

Admin Endpoints (ADMIN only)

MethodEndpointDescription
POST/admin/organizationsCreate organization
GET/admin/organizationsList all organizations
GET/admin/organizations/:idGet organization details
PUT/admin/organizations/:idUpdate organization
DELETE/admin/organizations/:idDelete organization
POST/admin/organizations/:id/usersAdd user to organization
DELETE/admin/organizations/:id/users/:userIdRemove user
POST/admin/organizations/transfer-userTransfer user between orgs

Organization Endpoints (ORG_ADMIN + ADMIN)

MethodEndpointDescription
GET/api/v1/organizations/meGet current user's org
GET/api/v1/organizations/:idGet org by ID
PUT/api/v1/organizations/:idUpdate org settings
GET/api/v1/organizations/:id/usersGet org users
GET/api/v1/organizations/:id/classesGet org classes
GET/api/v1/organizations/:id/invitationsGet org invitations
GET/api/v1/organizations/:id/statsGet org statistics

Backend Implementation

Module Structure

src/organizations/
├── organizations.module.ts
├── organizations.service.ts
├── organizations.controller.ts # ORG_ADMIN + ADMIN access
├── admin-organizations.controller.ts # ADMIN-only endpoints
├── index.ts
├── dto/
│ ├── create-organization.dto.ts
│ ├── update-organization.dto.ts
│ └── index.ts
└── guards/
└── organization.guard.ts

OrganizationGuard

Ensures users can only access resources within their organization:

@Injectable()
export class OrganizationGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const user = request.user;

// ADMIN bypasses all org checks
if (user.role === UserRole.ADMIN) return true;

// Verify user belongs to target organization
if (user.organizationId !== targetOrgId) {
throw new ForbiddenException('Access denied');
}

return true;
}
}

Frontend Implementation

Type Updates

// types/index.ts
export type UserRole = 'STUDENT' | 'COACH' | 'PARENT' | 'ADMIN' | 'ORG_ADMIN'

export interface Organization {
id: string
name: string
slug: string
settings?: OrganizationSettings
createdAt: string
updatedAt: string
}

export interface User {
// ... existing fields ...
organizationId?: string
organization?: Organization
}

New Pages

app/org-admin/
├── dashboard/page.tsx # Org stats and quick actions
├── users/page.tsx # Manage users within org
├── classes/page.tsx # Manage classes within org
├── invitations/page.tsx # Manage invitations within org
└── settings/page.tsx # Org settings (name, logo)

ORG_ADMIN users see organization management menu:

  • Dashboard
  • Users
  • Classes
  • Invitations
  • Settings

Migration Strategy

Phase 1: Schema Changes (Non-Breaking)

  1. Add Organization table
  2. Add nullable organizationId to User, Class, Workspace, Invitation

Phase 2: Data Migration

  1. Create default organization ("StemBlock AI")
  2. Backfill all existing users to default org
  3. Backfill all existing classes to default org

Phase 3: Constraint Updates

  1. Make User.organizationId required
  2. Make Class.organizationId required
  3. Add composite indexes

Rollback Strategy

-- Emergency rollback
ALTER TABLE "users" DROP COLUMN IF EXISTS "organization_id";
ALTER TABLE "classes" DROP COLUMN IF EXISTS "organization_id";
DROP TABLE IF EXISTS "organizations";

Test Users

RoleEmailPasswordTier
ADMINadmin@test.comAdmin123!ENTERPRISE
ORG_ADMINorgadmin@test.comOrgAdmin123!ENTERPRISE
COACHcoach@test.comCoach123!COMMUNITY
STUDENTalex@test.comStudent123!COMMUNITY
PARENTparent@test.comParent123!PRO

Files Modified

Backend

FileChange
prisma/schema.prismaAdded Organization model, ORG_ADMIN role, relations
prisma/seed.tsAdded default org, admin users
src/app.module.tsImport OrganizationsModule
src/organizations/*New module (service, controllers, guards, DTOs)

Frontend (Planned)

FileChange
types/index.tsAdd Organization interface, ORG_ADMIN role
lib/auth-context.tsxAdd organization to auth state
lib/api.tsAdd organizationsAPI, orgAdminAPI
components/Navigation.tsxAdd ORG_ADMIN navigation
app/org-admin/*New pages for org management

Future Enhancements

  1. Organization Hierarchy: Support for districts with multiple schools
  2. Cross-Organization Sharing: Allow sharing resources between organizations
  3. SSO Integration: SAML/OAuth for enterprise organizations
  4. White-Label Branding: Custom branding per organization
  5. Usage Analytics: Per-organization usage metrics and billing