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

TierScopeRole TablePermission Resolution
Super AdminGlobal (Teamcast internal)N/ABypasses all permission checks
PlatformPer-platform (e.g., LinkedIn)platform_rolesDB lookup: PlatformRole -> PlatformRolePermission -> Permission
TenantPer-tenant (e.g., Acme Corp)tenant_rolesDB lookup: TenantRole -> TenantRolePermission -> Permission
API KeyPer-platform keyN/AStored 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.

sql
-- 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.

sql
-- 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

sql
-- 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.

text
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 missing
Role permissions are cached in-memory with a 60-second TTL to avoid a database round-trip on every API request. Cache is per-roleId, not per-user.

Default 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

RolePermissionsUsers
Admin28 permissions (all)Tenant administrators
Recruiter9 permissions (interview:*, user:read, role:read)Interview operators
User2 permissions (interview:read, role:read)Read-only viewers

Platform Default Roles

RolePermissionsUsers
Admin21 permissions (tenant/user/apikey/interview/webhook/role)Platform administrators
Viewer6 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 RoleUse CaseTypical Permissions
Hiring ManagerApprove plans without conducting interviewsinterview:read, interview:approve, user:read
AssessorReview completed interviews and assessmentsinterview:read, interview:assess
AuditorRead-only access to all resourcesinterview:read, user:read, tenant:read, role:read
Team LeadManage team users and view interviewsuser: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.

typescript
// 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);
Users without a 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 MethodPermission SourceCeiling Check
JWT (Tenant User)User.roleId -> TenantRolePermission -> PermissionNone
JWT (Platform User)PlatformUser.platformRoleId -> PlatformRolePermission -> PermissionNone
JWT (Super Admin)Bypassed (all permissions)None
API KeyApiKey.permissions JSON arrayPlatform.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.

bash
# 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"
Was this page helpful?