Architecture
RBAC System
Database-driven Role-Based Access Control across three authorization tiers.
The platform implements a three-tier RBAC model where permissions are stored in PostgreSQL tables rather than hardcoded in application code. This allows tenants and platform admins to create custom roles with fine-grained permission assignment without code changes.
Authorization Tiers
| Tier | Scope | Role Table | Permission Resolution |
|---|---|---|---|
| Super Admin | Global (Teamcast internal) | N/A | Bypasses all permission checks |
| Platform | Per-platform (e.g., LinkedIn) | platform_roles | DB lookup: PlatformRole -> PlatformRolePermission -> Permission |
| Tenant | Per-tenant (e.g., Acme Corp) | tenant_roles | DB lookup: TenantRole -> TenantRolePermission -> Permission |
| API Key | Per-platform key | N/A | Stored as JSON array on ApiKey.permissions |
Database Schema
The RBAC system uses five core tables plus two join tables for the many-to-many relationship between roles and permissions.
Permission Table (Global)
Stores all available permissions. Not tenant-scoped - shared across the entire system. Seeded from the canonical permission list during deployment.
-- permissions table
id UUID PRIMARY KEY
code TEXT UNIQUE -- e.g. "interview:create"
resource TEXT -- e.g. "interview"
action TEXT -- e.g. "create"
description TEXT DEFAULT ''TenantRole Table
Tenant-scoped roles. Each tenant gets default system roles (Admin, Recruiter, User) and can create custom roles.
-- tenant_roles table
id UUID PRIMARY KEY
tenantId UUID REFERENCES tenants(id)
name TEXT -- unique per tenant
description TEXT DEFAULT ''
isSystem BOOLEAN DEFAULT false -- true = cannot be deleted
UNIQUE(tenantId, name)Join Tables
-- tenant_role_permissions (many-to-many)
tenantRoleId UUID REFERENCES tenant_roles(id) ON DELETE CASCADE
permissionId UUID REFERENCES permissions(id) ON DELETE CASCADE
UNIQUE(tenantRoleId, permissionId)
-- platform_role_permissions (many-to-many)
platformRoleId UUID REFERENCES platform_roles(id) ON DELETE CASCADE
permissionId UUID REFERENCES permissions(id) ON DELETE CASCADE
UNIQUE(platformRoleId, permissionId)Permission Resolution Flow
Every authenticated API request passes through the PermissionsGuard, which resolves the user's permissions based on their authentication method.
Request arrives
|
AuthGuard: Validate JWT or API Key
|
PermissionsGuard: Check @RequirePermissions() decorator
|
+-- API Key user: Check ApiKey.permissions JSON array
| (+ validate against Platform.allowedPermissions ceiling)
|
+-- Super Admin JWT: Allow all (bypass)
|
+-- Tenant User JWT:
1. Look up User in DB (by id + tenantId)
2. Read user.roleId
3. Query TenantRolePermission + Permission (with 60s cache)
4. Check required permissions against resolved set
5. Throw 403 if missingDefault Roles
When a new tenant is created, three system roles are automatically seeded. These cannot be deleted but their permissions can be modified.
Tenant Default Roles
| Role | Permissions | Users |
|---|---|---|
| Admin | 28 permissions (all) | Tenant administrators |
| Recruiter | 9 permissions (interview:*, user:read, role:read) | Interview operators |
| User | 2 permissions (interview:read, role:read) | Read-only viewers |
Platform Default Roles
| Role | Permissions | Users |
|---|---|---|
| Admin | 21 permissions (tenant/user/apikey/interview/webhook/role) | Platform administrators |
| Viewer | 6 permissions (read-only) | Platform read-only users |
Custom Roles
Tenant admins can create custom roles with any combination of available permissions. Use cases include:
| Custom Role | Use Case | Typical Permissions |
|---|---|---|
| Hiring Manager | Approve plans without conducting interviews | interview:read, interview:approve, user:read |
| Assessor | Review completed interviews and assessments | interview:read, interview:assess |
| Auditor | Read-only access to all resources | interview:read, user:read, tenant:read, role:read |
| Team Lead | Manage team users and view interviews | user:create, user:read, user:update, interview:read |
Integration with User Management
The User model maintains both a legacy role enum field and a new roleIdforeign key. The permission guard uses roleId as the single source of truth for permission resolution.
// User model (simplified)
model User {
role UserRole // Legacy enum: ADMIN | RECRUITER | USER
roleId String? // FK to TenantRole (source of truth for permissions)
// ...
}
// PermissionsGuard resolution
if (!dbUser.roleId) {
throw ForbiddenException('User has no role assigned');
}
const permissions = await getPermissionsFromDb(dbUser.roleId);roleId will be blocked from all permission-protected endpoints. Run the RBAC seed script to assign roleId to existing users.API Key Permissions
API keys use a separate permission model - permissions are stored directly as a JSON array on the ApiKey record. This is intentional: API keys represent integration partners, not individual users, and their permissions are set at key creation time.
| Auth Method | Permission Source | Ceiling Check |
|---|---|---|
| JWT (Tenant User) | User.roleId -> TenantRolePermission -> Permission | None |
| JWT (Platform User) | PlatformUser.platformRoleId -> PlatformRolePermission -> Permission | None |
| JWT (Super Admin) | Bypassed (all permissions) | None |
| API Key | ApiKey.permissions JSON array | Platform.allowedPermissions ceiling |
Seed Script
The RBAC seed script populates permissions and default roles for all existing tenants and platforms. It is idempotent and safe to re-run.
# Seed permissions + default roles for all tenants/platforms
npx ts-node prisma/seed-rbac.ts
# Output:
# Seeded 28 permissions
# Tenant "Demo Company": role "Admin" -> 28 permissions
# Tenant "Demo Company": role "Recruiter" -> 9 permissions
# Tenant "Demo Company": role "User" -> 2 permissions
# Assigned roleId to 3 existing users in "Demo Company"