Skip to main content

Overview

The Platform API uses a flexible Role-Based Access Control (RBAC) system with two permission scopes:
  • Company-scoped permissions: Managed through roles within each company (tenant)
  • Global permissions: Platform-level permissions assigned directly to users

Architecture

Permissions

Permission Model

Permissions are defined globally and can be assigned at two levels:
Permission Structure
{
  id: "uuid",
  key: "companies.create",
  description: "Create new companies",
  scope: "GLOBAL" | "COMPANY"
}
Key Fields:
  • key: Unique identifier in dot notation (e.g., members.invite, projects.delete)
  • description: Human-readable explanation of what the permission allows
  • scope: Determines how the permission can be assigned

Permission Scopes

Company-scoped permissions are managed through roles within each company.
  • Assigned to Roles, which are then assigned to Memberships
  • Only effective within a specific company context
  • Each company can configure roles independently
  • Default scope for most business logic permissions
Examples:
  • members.invite - Invite new members to the company
  • projects.create - Create projects within the company
  • time-entries.manage - Manage time entries for the company

Permission Assignment

// 1. Create or find role
const adminRole = await prisma.role.create({
  data: {
    companyId: "company-uuid",
    name: "Admin",
    description: "Company administrator"
  }
});

// 2. Assign permissions to role
await prisma.rolePermission.createMany({
  data: [
    { roleId: adminRole.id, permissionId: "members.invite" },
    { roleId: adminRole.id, permissionId: "projects.create" },
    { roleId: adminRole.id, permissionId: "roles.manage" }
  ]
});

// 3. Assign role to membership
await prisma.membershipRole.create({
  data: {
    membershipId: "membership-uuid",
    roleId: adminRole.id
  }
});

Roles

Role Model

Roles are company-specific and group related permissions:
Role Structure
{
  id: "uuid",
  companyId: "uuid",
  name: "Admin",
  description: "Company administrator with elevated privileges",
  color: "#F59E0B",
  isSystem: true,
  isDefault: false,
  createdAt: "2024-01-01T00:00:00Z",
  updatedAt: "2024-01-01T00:00:00Z"
}
Key Fields:
  • name: Must be unique within the company
  • color: Hex color for UI display (defaults to #6366F1)
  • isSystem: Protected system roles that cannot be deleted
  • isDefault: One role per company marked as default for new members

System Roles

When a company is created, four default roles are automatically generated:

Owner

Color: #EF4444 (Red)
System: Yes
Default: No
Full control over the company. Typically assigned to the company creator.

Admin

Color: #F59E0B (Orange)
System: Yes
Default: No
Elevated privileges for managing company resources and members.

Manager

Color: #3B82F6 (Blue)
System: No
Default: No
Team oversight and project management capabilities.

Member

Color: #6B7280 (Gray)
System: Yes
Default: Yes
Standard member with basic access. Automatically assigned to new members.

Role Management

POST /api/companies/:companyId/roles
{
  "name": "Developer",
  "description": "Software development team members",
  "color": "#10B981"
}
Role names must be unique within the company.
PATCH /api/companies/:companyId/roles/:roleId
{
  "name": "Senior Developer",
  "description": "Lead software developers",
  "color": "#059669"
}
System roles can be updated but not deleted.
DELETE /api/companies/:companyId/roles/:roleId
Deletion fails if:
  • Role has isSystem: true
  • Role is assigned to any members (delete restriction)
PATCH /api/companies/:companyId/roles/:roleId
{
  "isDefault": true
}
Only one role per company can be marked as default. The previous default is automatically unmarked.

Membership Roles

Many-to-Many Relationship

A membership can have multiple roles, and a role can be assigned to multiple memberships:
MembershipRole Join Table
{
  membershipId: "uuid",
  roleId: "uuid"
}
Database Constraints:
  • Primary key on (membershipId, roleId) prevents duplicates
  • ON DELETE Cascade on membership - removing membership deletes role assignments
  • ON DELETE Restrict on role - cannot delete role if assigned to any member

Assigning Roles to Members

Update Member Roles
PUT /api/companies/:companyId/members/:memberId/roles
{
  "roleIds": [
    "admin-role-uuid",
    "developer-role-uuid"
  ]
}
This endpoint:
  1. Removes all existing role assignments for the member
  2. Creates new assignments for the provided role IDs
  3. Returns the updated membership with new roles

Permission Checks

Global Permission Check

Use the checkGlobalPermission middleware for platform-level operations:
import { checkGlobalPermission } from '@/common/middlewares/check-global-permission.middleware';

router.post(
  '/companies',
  authMiddleware,
  checkGlobalPermission('companies.create'),
  asyncHandler(companiesController.create)
);
How it works:
  1. Checks if user is a platform admin (bypass)
  2. Looks up permission by key with scope: GLOBAL
  3. Verifies UserGlobalPermission exists for the user
  4. Throws 403 Forbidden if permission not found

Company Permission Check

For company-scoped permissions, check through membership roles:
const hasPermission = async (
  userId: string,
  companyId: string,
  permissionKey: string
): Promise<boolean> => {
  // 1. Platform admins have all permissions
  const platformAdmin = await prisma.platformAdmin.findUnique({
    where: { userId }
  });
  if (platformAdmin) return true;

  // 2. Find user's membership
  const membership = await prisma.membership.findUnique({
    where: {
      companyId_userId: { companyId, userId }
    },
    include: {
      roles: {
        include: {
          role: {
            include: {
              permissions: {
                include: { permission: true }
              }
            }
          }
        }
      }
    }
  });

  if (!membership) return false;

  // 3. Check if any role has the permission
  return membership.roles.some(mr =>
    mr.role.permissions.some(rp =>
      rp.permission.key === permissionKey
    )
  );
};

Platform Admin Bypass

Platform administrators automatically pass all permission checks:
const platformAdmin = await prisma.platformAdmin.findUnique({
  where: { userId }
});

if (platformAdmin) {
  return next(); // Bypass all checks
}
This allows admins to:
  • Access all companies and their resources
  • Perform any action regardless of role assignments
  • Manage system-level configurations

Permission Request System

Users can request global permissions through a formal approval workflow:

Request Flow

1

User Creates Request

POST /api/permission-requests
{
  "type": "GLOBAL_PERMISSION",
  "requestedPermissionId": "uuid",
  "reason": "I need to create companies for our enterprise customers"
}
2

Admin Reviews Request

Platform admins can view all pending requests:
GET /api/permission-requests?status=PENDING
3

Approve or Reject

POST /api/permission-requests/:id/review
{
  "action": "approve",
  "reviewNotes": "Approved for enterprise support role"
}
4

Automatic Assignment

If approved, the system automatically creates the UserGlobalPermission record and updates the request status to APPROVED.
Request Statuses:
  • PENDING - Awaiting admin review
  • APPROVED - Admin approved, permission granted
  • REJECTED - Admin rejected with reason
  • CANCELLED - User cancelled their own request

Best Practices

Principle of Least Privilege

Grant only the minimum permissions required for a role. Start with restricted access and expand as needed.

Use Descriptive Keys

Permission keys should follow the pattern resource.action (e.g., members.invite, projects.delete) for clarity.

Protect System Roles

Mark critical roles with isSystem: true to prevent accidental deletion. Validate this flag in your application logic.

Audit Permission Changes

Log all permission grants, revocations, and role assignments for security auditing and compliance.

Multi-Tenancy

Learn how companies provide tenant isolation

Memberships

Understand how roles connect to user memberships

Invitations

See how default roles are assigned during invitation

Build docs developers (and LLMs) love