AI Development GuideFoundations
Database Schema
Complete Prisma schema and Row-Level Security for HRMS
Database Schema
This is the complete Prisma schema for the HRMS system. Copy this exactly when setting up the database.
Prisma Configuration
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Tenant Model
model Tenant {
id String @id @default(cuid())
name String
domain String? @unique
status TenantStatus @default(ACTIVE)
settings Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
employees Employee[]
departments Department[]
teams Team[]
roles OrgRole[]
timeOffPolicies TimeOffPolicy[]
documents Document[]
posts Post[]
goals Goal[]
workflowTemplates WorkflowTemplate[]
tagCategories TagCategory[]
tags Tag[]
tagPermissions TagPermission[]
customFieldDefs CustomFieldDefinition[]
customFieldValues CustomFieldValue[]
dashboards Dashboard[]
@@map("tenants")
}
enum TenantStatus {
ACTIVE
SUSPENDED
TRIAL
}User & Auth Models
Note: The User model uses optional tenantId because Auth.js PrismaAdapter creates the user record BEFORE the
createUserevent fires. The event handler then updates the user with tenant context. See Phase 01: Multi-Tenant Auth for details.
model User {
id String @id @default(cuid())
tenantId String? // Optional - Auth.js creates user BEFORE createUser event assigns tenant
email String
emailVerified DateTime? // Required by Auth.js adapter
name String?
image String?
systemRole SystemRole @default(EMPLOYEE)
status UserStatus @default(ACTIVE)
employeeId String? @unique
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
employee Employee? @relation(fields: [employeeId], references: [id])
accounts Account[]
sessions Session[]
dashboards Dashboard[]
@@unique([email]) // Email unique across system (simpler for MVP)
@@index([tenantId]) // Index for tenant queries
@@map("users")
}
enum SystemRole {
SYSTEM_ADMIN
HR_ADMIN
MANAGER
EMPLOYEE
}
enum UserStatus {
ACTIVE
INACTIVE
PENDING
}Employee Model
model Employee {
id String @id @default(cuid())
tenantId String
employeeNumber String?
// Personal Info
firstName String
lastName String
email String
phone String?
pictureUrl String?
// Emergency Contact
emergencyContactName String?
emergencyContactPhone String?
emergencyContactRelation String? // e.g., "Spouse", "Parent", "Sibling"
// Job Information
jobTitle String?
jobFamily String?
jobLevel String?
// Employment Details
employmentType EmploymentType @default(FULL_TIME)
workMode WorkMode @default(ONSITE)
status EmployeeStatus @default(ACTIVE)
hireDate DateTime?
terminationDate DateTime?
// Metadata
customFields Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
user User?
// Org Relations
orgRelations EmployeeOrgRelations?
// As a manager (receiving reports)
primaryReports EmployeeOrgRelations[] @relation("PrimaryManager")
dottedLineReports EmployeeDottedLine[] @relation("DottedLineManager")
additionalReports EmployeeAdditionalManager[] @relation("AdditionalManager")
// Leadership positions
ledTeams Team[] @relation("TeamLead")
ledDepartments Department[] @relation("DepartmentHead")
// Other relations
timeOffRequests TimeOffRequest[]
timeOffBalances TimeOffBalance[]
posts Post[]
comments Comment[]
goals Goal[]
skills EmployeeSkill[]
tags EmployeeTag[]
@@unique([tenantId, email])
@@unique([tenantId, employeeNumber])
@@index([tenantId, status])
@@map("employees")
}
enum EmploymentType {
FULL_TIME
PART_TIME
CONTRACTOR
INTERN
TEMPORARY
}
enum WorkMode {
ONSITE
REMOTE
HYBRID
}
enum EmployeeStatus {
ACTIVE
INACTIVE
ON_LEAVE
TERMINATED
}Org Relations Models
model EmployeeOrgRelations {
id String @id @default(cuid())
employeeId String @unique
// Primary (direct) manager - 0..1
primaryManagerId String?
updatedAt DateTime @updatedAt
// Relations
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
primaryManager Employee? @relation("PrimaryManager", fields: [primaryManagerId], references: [id])
// Many-to-many via junction tables
dottedLineManagers EmployeeDottedLine[]
additionalManagers EmployeeAdditionalManager[]
departments EmployeeDepartment[]
teams EmployeeTeam[]
roles EmployeeOrgRole[]
@@map("employee_org_relations")
}
// Junction: Dotted Line Managers (0..N)
model EmployeeDottedLine {
id String @id @default(cuid())
orgRelationsId String
managerId String
createdAt DateTime @default(now())
orgRelations EmployeeOrgRelations @relation(fields: [orgRelationsId], references: [id], onDelete: Cascade)
manager Employee @relation("DottedLineManager", fields: [managerId], references: [id])
@@unique([orgRelationsId, managerId])
@@map("employee_dotted_lines")
}
// Junction: Additional Real Managers (0..N)
model EmployeeAdditionalManager {
id String @id @default(cuid())
orgRelationsId String
managerId String
createdAt DateTime @default(now())
orgRelations EmployeeOrgRelations @relation(fields: [orgRelationsId], references: [id], onDelete: Cascade)
manager Employee @relation("AdditionalManager", fields: [managerId], references: [id])
@@unique([orgRelationsId, managerId])
@@map("employee_additional_managers")
}
// Junction: Employee <-> Department (0..N)
model EmployeeDepartment {
id String @id @default(cuid())
orgRelationsId String
departmentId String
isPrimary Boolean @default(false)
createdAt DateTime @default(now())
orgRelations EmployeeOrgRelations @relation(fields: [orgRelationsId], references: [id], onDelete: Cascade)
department Department @relation(fields: [departmentId], references: [id])
@@unique([orgRelationsId, departmentId])
@@map("employee_departments")
}
// Junction: Employee <-> Team (0..N)
model EmployeeTeam {
id String @id @default(cuid())
orgRelationsId String
teamId String
role String? // Role within team: "Tech Lead", "Member"
createdAt DateTime @default(now())
orgRelations EmployeeOrgRelations @relation(fields: [orgRelationsId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id])
@@unique([orgRelationsId, teamId])
@@map("employee_teams")
}
// Junction: Employee <-> OrgRole (0..N)
model EmployeeOrgRole {
id String @id @default(cuid())
orgRelationsId String
roleId String
isPrimary Boolean @default(false)
createdAt DateTime @default(now())
orgRelations EmployeeOrgRelations @relation(fields: [orgRelationsId], references: [id], onDelete: Cascade)
role OrgRole @relation(fields: [roleId], references: [id])
@@unique([orgRelationsId, roleId])
@@map("employee_org_roles")
}Department & Team Models
model Department {
id String @id @default(cuid())
tenantId String
name String
code String?
description String?
parentId String?
headId String?
status DepartmentStatus @default(ACTIVE)
customFields Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
parent Department? @relation("DepartmentHierarchy", fields: [parentId], references: [id])
children Department[] @relation("DepartmentHierarchy")
head Employee? @relation("DepartmentHead", fields: [headId], references: [id])
employeeDepartments EmployeeDepartment[]
@@unique([tenantId, code])
@@map("departments")
}
enum DepartmentStatus {
ACTIVE
INACTIVE
}
model Team {
id String @id @default(cuid())
tenantId String
name String
description String?
type TeamType @default(PERMANENT)
parentId String?
leadId String?
status TeamStatus @default(ACTIVE)
customFields Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
parent Team? @relation("TeamHierarchy", fields: [parentId], references: [id])
children Team[] @relation("TeamHierarchy")
lead Employee? @relation("TeamLead", fields: [leadId], references: [id])
employeeTeams EmployeeTeam[]
@@map("teams")
}
enum TeamType {
PERMANENT
PROJECT
SQUAD
GUILD
TRIBE
}
enum TeamStatus {
ACTIVE
INACTIVE
ARCHIVED
}
model OrgRole {
id String @id @default(cuid())
tenantId String
name String
category String?
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
employeeRoles EmployeeOrgRole[]
@@unique([tenantId, name])
@@map("org_roles")
}Time-Off Models
Multi-Tenant Note:
TimeOffBalanceandTimeOffRequestincludetenantIdwith aTenantrelation for proper data isolation. All queries MUST filter bytenantIdto ensure tenant isolation.
model TimeOffPolicy {
id String @id @default(cuid())
tenantId String
name String
code String?
description String?
leaveType LeaveType
accrualType AccrualType @default(ANNUAL)
annualAllowance Float @default(0)
carryOverLimit Float @default(0)
carryOverExpiry Int?
requiresApproval Boolean @default(true)
autoApproveRules Json?
appliesTo Json @default("{}")
status PolicyStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
requests TimeOffRequest[]
balances TimeOffBalance[]
@@unique([tenantId, code])
@@map("time_off_policies")
}
enum LeaveType {
VACATION
SICK
PERSONAL
PARENTAL
MATERNITY
PATERNITY
BEREAVEMENT
UNPAID
COMPENSATORY
OTHER
}
enum AccrualType {
ANNUAL
MONTHLY
DAILY
NONE
}
enum PolicyStatus {
ACTIVE
INACTIVE
ARCHIVED
}
model TimeOffRequest {
id String @id @default(cuid())
tenantId String
employeeId String
policyId String
startDate DateTime
endDate DateTime
days Float
halfDay Boolean @default(false)
halfDayPart HalfDayPart?
status RequestStatus @default(PENDING)
reason String?
approverId String?
approvedAt DateTime?
approverComment String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
policy TimeOffPolicy @relation(fields: [policyId], references: [id])
@@index([tenantId, status])
@@map("time_off_requests")
}
enum RequestStatus {
PENDING
APPROVED
REJECTED
CANCELLED
EXPIRED
}
enum HalfDayPart {
MORNING
AFTERNOON
}
model TimeOffBalance {
id String @id @default(cuid())
tenantId String
employeeId String
policyId String
year Int
entitled Float @default(0)
used Float @default(0)
pending Float @default(0)
carryOver Float @default(0)
adjustment Float @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
policy TimeOffPolicy @relation(fields: [policyId], references: [id])
@@unique([employeeId, policyId, year])
@@map("time_off_balances")
}Tag System Models
model TagCategory {
id String @id @default(cuid())
tenantId String
name String // "skills", "departments", "status", etc.
assetTypes String[] // ["employee", "document", "goal"]
color String? // Default color for tags in this category
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tags Tag[]
@@unique([tenantId, name])
@@index([tenantId])
@@map("tag_categories")
}
model Tag {
id String @id @default(cuid())
tenantId String
categoryId String
name String
color String? // Override category color
description String?
status TagStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
category TagCategory @relation(fields: [categoryId], references: [id])
permissions TagPermission[]
employees EmployeeTag[]
documents DocumentTag[]
@@unique([tenantId, categoryId, name])
@@index([tenantId])
@@index([categoryId])
@@map("tags")
}
enum TagStatus {
ACTIVE
ARCHIVED
}
model TagPermission {
id String @id @default(cuid())
tenantId String
tagId String
role SystemRole // Who can use this tag
canAssign Boolean @default(true)
canRemove Boolean @default(true)
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@unique([tenantId, tagId, role])
@@index([tenantId])
@@map("tag_permissions")
}
model EmployeeTag {
employeeId String
tagId String
assignedAt DateTime @default(now())
assignedBy String?
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([employeeId, tagId])
@@map("employee_tags")
}
model DocumentTag {
documentId String
tagId String
assignedAt DateTime @default(now())
assignedBy String?
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([documentId, tagId])
@@map("document_tags")
}Custom Fields Models
model CustomFieldDefinition {
id String @id @default(cuid())
tenantId String
entityType EntityType
fieldName String
fieldType FieldType
label String
description String?
required Boolean @default(false)
visibility FieldVisibility @default(ALL)
editable Boolean @default(true)
options Json? // For DROPDOWN: ["option1", "option2"]
validation Json? // { min, max, pattern, etc. }
section String? // UI grouping
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
values CustomFieldValue[]
@@unique([tenantId, entityType, fieldName])
@@index([tenantId])
@@map("custom_field_definitions")
}
model CustomFieldValue {
id String @id @default(cuid())
tenantId String
fieldId String
entityType EntityType
entityId String // employeeId, departmentId, teamId, goalId
value String? // Stored as JSON-compatible string
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
field CustomFieldDefinition @relation(fields: [fieldId], references: [id], onDelete: Cascade)
@@unique([tenantId, fieldId, entityType, entityId])
@@index([tenantId])
@@index([entityType, entityId])
@@map("custom_field_values")
}
enum EntityType {
EMPLOYEE
DEPARTMENT
TEAM
GOAL
}
enum FieldType {
TEXT
TEXTAREA
NUMBER
DATE
DROPDOWN
CHECKBOX
URL
EMAIL
}
enum FieldVisibility {
ALL
MANAGER_PLUS
HR_ADMIN_ONLY
}Document Models (with Access Control)
model Document {
id String @id @default(cuid())
tenantId String
employeeId String?
uploadedById String
title String
description String?
category DocumentCategory
fileUrl String
fileName String
mimeType String
fileSize Int
visibility DocumentVisibility @default(PRIVATE)
vectorId String?
indexed Boolean @default(false)
status DocumentStatus @default(ACTIVE)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
tags DocumentTag[]
accessGrants DocumentAccess[]
@@index([tenantId, category])
@@index([tenantId, visibility])
@@map("documents")
}
model DocumentAccess {
id String @id @default(cuid())
documentId String
accessType AccessType
targetId String? // userId, teamId, departmentId, roleId
permission DocumentPermission @default(VIEW)
grantedBy String
grantedAt DateTime @default(now())
expiresAt DateTime?
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
@@index([documentId])
@@index([targetId])
@@map("document_access")
}
enum DocumentCategory {
POLICY
HANDBOOK
CONTRACT
PERFORMANCE
TRAINING
CERTIFICATE
PERSONAL
OTHER
}
enum DocumentVisibility {
PRIVATE // Only document owner
TEAM // Owner's team(s)
DEPARTMENT // Owner's department(s)
MANAGERS // All employees with manager role
COMPANY // All employees in tenant
CUSTOM // Use DocumentAccess table for explicit grants
}
enum AccessType {
USER
TEAM
DEPARTMENT
ROLE
}
enum DocumentPermission {
VIEW
DOWNLOAD
EDIT
}
enum DocumentStatus {
ACTIVE
ARCHIVED
DELETED
}
model Post {
id String @id @default(cuid())
tenantId String
authorId String
type PostType @default(TEXT)
content String
mediaUrls String[] @default([])
scope PostScope @default(COMPANY)
scopeRefId String?
sentiment Sentiment?
summary String?
likeCount Int @default(0)
likedBy String[] @default([])
status PostStatus @default(PUBLISHED)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
author Employee @relation(fields: [authorId], references: [id])
comments Comment[]
@@index([tenantId, scope, createdAt])
@@map("posts")
}
enum PostType {
TEXT
IMAGE
VIDEO
ANNOUNCEMENT
POLL
}
enum PostScope {
COMPANY
DEPARTMENT
TEAM
}
enum Sentiment {
POSITIVE
NEUTRAL
NEGATIVE
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
DELETED
}
model Comment {
id String @id @default(cuid())
postId String
authorId String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
author Employee @relation(fields: [authorId], references: [id])
@@map("comments")
}
model Goal {
id String @id @default(cuid())
tenantId String
ownerId String
title String
description String?
level GoalLevel @default(PERSONAL)
progress Float @default(0)
status GoalStatus @default(DRAFT)
startDate DateTime?
dueDate DateTime?
completedAt DateTime?
parentId String?
tags String[] @default([])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
owner Employee @relation(fields: [ownerId], references: [id])
parent Goal? @relation("GoalHierarchy", fields: [parentId], references: [id])
children Goal[] @relation("GoalHierarchy")
keyResults KeyResult[]
@@index([tenantId, ownerId])
@@map("goals")
}
enum GoalLevel {
COMPANY
DEPARTMENT
TEAM
PERSONAL
}
enum GoalStatus {
DRAFT
ACTIVE
COMPLETED
CANCELLED
ON_HOLD
}
model KeyResult {
id String @id @default(cuid())
goalId String
title String
targetValue Float @default(100)
currentValue Float @default(0)
unit String?
status KeyResultStatus @default(ON_TRACK)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
goal Goal @relation(fields: [goalId], references: [id], onDelete: Cascade)
@@map("key_results")
}
enum KeyResultStatus {
ON_TRACK
AT_RISK
OFF_TRACK
COMPLETED
}Workflow & Skills Models
model Skill {
id String @id @default(cuid())
tenantId String?
name String
category String?
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
employeeSkills EmployeeSkill[]
@@unique([tenantId, name])
@@map("skills")
}
model EmployeeSkill {
id String @id @default(cuid())
employeeId String
skillId String
level Int @default(1)
source SkillSource @default(SELF)
validatedAt DateTime?
validatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
skill Skill @relation(fields: [skillId], references: [id])
@@unique([employeeId, skillId])
@@map("employee_skills")
}
enum SkillSource {
SELF
MANAGER
PEER
AI_INFERRED
ASSESSMENT
}
model WorkflowTemplate {
id String @id @default(cuid())
tenantId String
name String
description String?
triggerType TriggerType
triggerConfig Json @default("{}")
steps Json
isActive Boolean @default(true)
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
instances WorkflowInstance[]
@@map("workflow_templates")
}
enum TriggerType {
EMPLOYEE_CREATED
EMPLOYEE_UPDATED
EMPLOYEE_TERMINATED
TIMEOFF_REQUESTED
TIMEOFF_APPROVED
GOAL_CREATED
SCHEDULED
MANUAL
}
model WorkflowInstance {
id String @id @default(cuid())
templateId String
entityType String
entityId String
status WorkflowStatus @default(PENDING)
currentStep Int @default(0)
stepResults Json @default("[]")
error String?
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
template WorkflowTemplate @relation(fields: [templateId], references: [id])
@@map("workflow_instances")
}
enum WorkflowStatus {
PENDING
RUNNING
PAUSED
COMPLETED
FAILED
CANCELLED
}Dashboard Models
model Dashboard {
id String @id @default(cuid())
tenantId String
userId String
name String
description String?
isDefault Boolean @default(false)
layout Json // Grid layout configuration
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
widgets DashboardWidget[]
shares DashboardShare[]
@@index([tenantId])
@@index([userId])
@@map("dashboards")
}
model DashboardWidget {
id String @id @default(cuid())
dashboardId String
widgetType String // "headcount", "turnover", "timeoff_calendar", "pending_approvals", etc.
title String? // Optional title override
config Json // Widget-specific configuration
position Json // { x, y, w, h } for grid layout
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
@@index([dashboardId])
@@map("dashboard_widgets")
}
model DashboardShare {
id String @id @default(cuid())
dashboardId String
shareType ShareType
targetId String? // userId, teamId, departmentId
permission SharePermission @default(VIEW)
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
@@index([dashboardId])
@@index([targetId])
@@map("dashboard_shares")
}
enum ShareType {
USER
TEAM
DEPARTMENT
COMPANY
}
enum SharePermission {
VIEW
EDIT
}Auth.js & Audit Models
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}
model AuditLog {
id String @id @default(cuid())
tenantId String
userId String?
action String
entityType String
entityId String
changes Json?
metadata Json?
createdAt DateTime @default(now())
@@index([tenantId, entityType, createdAt])
@@index([tenantId, userId, createdAt])
@@map("audit_logs")
}Row-Level Security
Apply these policies after running migrations:
-- Enable RLS on all tenant-scoped tables
ALTER TABLE employees ENABLE ROW LEVEL SECURITY;
ALTER TABLE employee_org_relations ENABLE ROW LEVEL SECURITY;
ALTER TABLE departments ENABLE ROW LEVEL SECURITY;
ALTER TABLE teams ENABLE ROW LEVEL SECURITY;
ALTER TABLE time_off_policies ENABLE ROW LEVEL SECURITY;
ALTER TABLE time_off_requests ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE goals ENABLE ROW LEVEL SECURITY;
-- Tenant ID function
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS TEXT AS $$
SELECT current_setting('app.current_tenant_id', true)
$$ LANGUAGE SQL SECURITY DEFINER;
-- Apply policy (repeat for all tables)
CREATE POLICY tenant_isolation ON employees
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON departments
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON teams
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON time_off_policies
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON time_off_requests
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON posts
USING (tenant_id = current_tenant_id());
CREATE POLICY tenant_isolation ON goals
USING (tenant_id = current_tenant_id());Setting Tenant Context
In NestJS middleware:
// Set tenant context before queries
await prisma.$executeRaw`SELECT set_config('app.current_tenant_id', ${tenantId}, true)`