Skip to main content

Overview

The Platform API implements a flexible Role-Based Access Control (RBAC) system with:
  • Global Permissions - Platform-level permissions assigned directly to users
  • Company Permissions - Tenant-scoped permissions assigned via roles
  • Custom Roles - Company-specific roles with configurable permissions
  • Permission Requests - User-initiated requests for elevated access

Permission Architecture

Permission Scopes

Permissions have two scope types:
scope
enum
  • GLOBAL - Platform-level permissions (e.g., create companies, manage all users)
  • COMPANY - Company-scoped permissions (e.g., manage members, view reports)

Permission Key Format

Permissions use a structured key format:
RESOURCE:ACTION
COMPANY:CREATE      // Create companies
COMPANY:DELETE      // Delete companies
MEMBER:INVITE       // Invite members
MEMBER:REMOVE       // Remove members
ROLE:CREATE         // Create roles
ROLE:ASSIGN         // Assign roles to members
PROJECT:CREATE      // Create projects
PROJECT:DELETE      // Delete projects
TIME_ENTRY:APPROVE  // Approve time entries
REPORT:VIEW         // View reports

Creating Permissions

Create Global Permission

Platform admins can create new permissions:
Request
POST /api/permissions
Authorization: Bearer {admin-token}
Content-Type: application/json

{
  "key": "REPORT:EXPORT",
  "description": "Export reports to various formats",
  "scope": "COMPANY"
}
Response
{
  "success": true,
  "data": {
    "id": "perm-123",
    "key": "REPORT:EXPORT",
    "description": "Export reports to various formats",
    "scope": "COMPANY",
    "_count": {
      "roles": 0,
      "userGlobalPermissions": 0
    }
  }
}
Permission keys must be unique and follow the RESOURCE:ACTION format with uppercase letters and underscores only.

List All Permissions

Request
GET /api/permissions?page=1&limit=50&scope=COMPANY
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": [
    {
      "id": "perm-001",
      "key": "COMPANY:CREATE",
      "description": "Create new companies",
      "scope": "GLOBAL",
      "_count": {
        "roles": 0,
        "userGlobalPermissions": 5
      }
    },
    {
      "id": "perm-002",
      "key": "MEMBER:INVITE",
      "description": "Invite members to company",
      "scope": "COMPANY",
      "_count": {
        "roles": 3,
        "userGlobalPermissions": 0
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 2,
    "totalPages": 1
  }
}

Get All Permissions (No Pagination)

Useful for dropdown menus and selection interfaces:
Request
GET /api/permissions/all
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": [
    {
      "id": "perm-001",
      "key": "COMPANY:CREATE",
      "description": "Create new companies",
      "scope": "GLOBAL"
    },
    {
      "id": "perm-002",
      "key": "MEMBER:INVITE",
      "description": "Invite members to company",
      "scope": "COMPANY"
    }
  ]
}

Managing Roles

List Company Roles

Request
GET /api/companies/{companyId}/roles
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": [
    {
      "id": "role-owner",
      "companyId": "company-789",
      "name": "Owner",
      "description": "Company owner with full access",
      "color": "#EF4444",
      "isSystem": true,
      "isDefault": false,
      "createdAt": "2026-03-01T10:00:00Z",
      "updatedAt": "2026-03-01T10:00:00Z"
    },
    {
      "id": "role-member",
      "companyId": "company-789",
      "name": "Member",
      "description": "Standard member",
      "color": "#6B7280",
      "isSystem": true,
      "isDefault": true,
      "createdAt": "2026-03-01T10:00:00Z",
      "updatedAt": "2026-03-01T10:00:00Z"
    }
  ]
}

Create Custom Role

Create a new role for your company:
Request
POST /api/companies/{companyId}/roles
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "Project Manager",
  "description": "Manages projects and team resources",
  "color": "#8B5CF6"
}
Response
{
  "success": true,
  "data": {
    "id": "role-pm",
    "companyId": "company-789",
    "name": "Project Manager",
    "description": "Manages projects and team resources",
    "color": "#8B5CF6",
    "isSystem": false,
    "isDefault": false,
    "createdAt": "2026-03-04T20:00:00Z",
    "updatedAt": "2026-03-04T20:00:00Z"
  }
}
Role names must be unique within a company. Colors should be hex format (#RRGGBB).

Update Role

Request
PATCH /api/companies/{companyId}/roles/{roleId}
Authorization: Bearer {token}
Content-Type: application/json

{
  "name": "Senior Project Manager",
  "description": "Senior PM with additional oversight",
  "color": "#7C3AED"
}
Response
{
  "success": true,
  "data": {
    "id": "role-pm",
    "name": "Senior Project Manager",
    "description": "Senior PM with additional oversight",
    "color": "#7C3AED",
    "updatedAt": "2026-03-04T21:00:00Z"
  }
}

Delete Role

Request
DELETE /api/companies/{companyId}/roles/{roleId}
Authorization: Bearer {token}
Response
HTTP/1.1 204 No Content
Cannot delete if:
  • Role is a system role (isSystem: true)
  • Role is assigned to any members
  • Role is set as the default role

Role Properties

isSystem
boolean
default:"false"
System roles (Owner, Admin, Member) are protected from deletion
isDefault
boolean
default:"false"
Default role is automatically assigned to new members. Only one per company.
color
string
default:"#6366F1"
Hex color code for UI display (e.g., #EF4444)

Assigning Permissions to Roles

Add Permissions to Role

Permissions are assigned to roles through the role-permission relationship:
Service Example
// In your application service layer
await prisma.rolePermission.createMany({
  data: [
    {
      roleId: "role-pm",
      permissionId: "perm-project-create"
    },
    {
      roleId: "role-pm",
      permissionId: "perm-project-update"
    },
    {
      roleId: "role-pm",
      permissionId: "perm-member-invite"
    }
  ],
  skipDuplicates: true
});

Get Role with Permissions

Query Example
const roleWithPermissions = await prisma.role.findUnique({
  where: { id: "role-pm" },
  include: {
    permissions: {
      include: {
        permission: true
      }
    }
  }
});

// Result structure
{
  id: "role-pm",
  name: "Project Manager",
  permissions: [
    {
      permission: {
        id: "perm-001",
        key: "PROJECT:CREATE",
        description: "Create new projects"
      }
    },
    {
      permission: {
        id: "perm-002",
        key: "PROJECT:UPDATE",
        description: "Update existing projects"
      }
    }
  ]
}

Remove Permission from Role

Service Example
await prisma.rolePermission.delete({
  where: {
    roleId_permissionId: {
      roleId: "role-pm",
      permissionId: "perm-member-invite"
    }
  }
});
Removing a permission from a role immediately affects all members with that role.

Global Permissions

Grant Global Permission to User

Platform admins can grant global permissions directly to users:
Service Example
await prisma.userGlobalPermission.create({
  data: {
    userId: "user-123",
    permissionId: "perm-company-create",
    grantedBy: "admin-user-id"
  }
});

Check Global Permissions

Middleware Example
export function checkGlobalPermission(permissionKey: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.userId;
    
    // Check if user is platform admin (bypass)
    if (req.isPlatformAdmin) {
      return next();
    }
    
    // Check if user has the global permission
    const hasPermission = await prisma.userGlobalPermission.findFirst({
      where: {
        userId,
        permission: {
          key: permissionKey
        }
      }
    });
    
    if (!hasPermission) {
      return res.status(403).json({
        success: false,
        error: 'Insufficient permissions'
      });
    }
    
    next();
  };
}

List User’s Global Permissions

Query Example
const userPermissions = await prisma.userGlobalPermission.findMany({
  where: { userId: "user-123" },
  include: {
    permission: true
  }
});

// Result
[
  {
    userId: "user-123",
    permissionId: "perm-001",
    grantedAt: "2026-03-01T10:00:00Z",
    grantedBy: "admin-user-id",
    permission: {
      id: "perm-001",
      key: "COMPANY:CREATE",
      description: "Create new companies",
      scope: "GLOBAL"
    }
  }
]

Permission Requests

Get Available Permissions

Users can view permissions they can request:
Request
GET /api/permission-requests/available-permissions
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": [
    {
      "id": "perm-001",
      "key": "COMPANY:CREATE",
      "description": "Create new companies",
      "scope": "GLOBAL"
    },
    {
      "id": "perm-002",
      "key": "REPORT:EXPORT",
      "description": "Export reports",
      "scope": "COMPANY"
    }
  ]
}

Create Permission Request

Users can request global permissions:
Request
POST /api/permission-requests
Authorization: Bearer {token}
Content-Type: application/json

{
  "type": "GLOBAL_PERMISSION",
  "requestedPermissionId": "perm-001",
  "reason": "Need to create companies for client projects"
}
Response
{
  "success": true,
  "data": {
    "id": "req-123",
    "userId": "user-123",
    "type": "GLOBAL_PERMISSION",
    "status": "PENDING",
    "requestedPermissionId": "perm-001",
    "reason": "Need to create companies for client projects",
    "createdAt": "2026-03-04T18:41:00Z"
  },
  "message": "Permission request submitted successfully. An admin will review it soon."
}

List User’s Permission Requests

Request
GET /api/permission-requests?status=PENDING
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": [
    {
      "id": "req-123",
      "type": "GLOBAL_PERMISSION",
      "status": "PENDING",
      "requestedPermission": {
        "id": "perm-001",
        "key": "COMPANY:CREATE",
        "description": "Create new companies"
      },
      "reason": "Need to create companies for client projects",
      "createdAt": "2026-03-04T18:41:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1
  }
}

Admin Reviews Permission Request

Platform admins can approve or reject requests:
Request
POST /api/permission-requests/admin/{requestId}/review
Authorization: Bearer {admin-token}
Content-Type: application/json

{
  "action": "approve",
  "reviewNotes": "Approved for Q1 client projects"
}
Response
{
  "success": true,
  "data": {
    "id": "req-123",
    "status": "APPROVED",
    "reviewedBy": "admin-user-id",
    "reviewedAt": "2026-03-04T19:00:00Z",
    "reviewNotes": "Approved for Q1 client projects"
  },
  "message": "Permission request approved and permission granted to user."
}
When a permission request is approved, the system automatically:
  • Updates request status to APPROVED
  • Creates a UserGlobalPermission record
  • Grants the user immediate access

Cancel Permission Request

Request
POST /api/permission-requests/{requestId}/cancel
Authorization: Bearer {token}
Response
{
  "success": true,
  "data": {
    "id": "req-123",
    "status": "CANCELLED"
  },
  "message": "Permission request cancelled"
}

Request Status Values

status
enum
  • PENDING - Awaiting admin review
  • APPROVED - Request approved, permission granted
  • REJECTED - Request denied by admin
  • CANCELLED - Request cancelled by user

Permission Checking

Check User’s Company Permissions

Helper Function
async function hasCompanyPermission(
  userId: string,
  companyId: string,
  permissionKey: string
): Promise<boolean> {
  const membership = await prisma.membership.findUnique({
    where: {
      companyId_userId: { companyId, userId }
    },
    include: {
      roles: {
        include: {
          role: {
            include: {
              permissions: {
                include: {
                  permission: true
                }
              }
            }
          }
        }
      }
    }
  });

  if (!membership) return false;

  // Check if any of the user's roles have the permission
  for (const membershipRole of membership.roles) {
    const hasPermission = membershipRole.role.permissions.some(
      rp => rp.permission.key === permissionKey
    );
    if (hasPermission) return true;
  }

  return false;
}

Usage in Middleware

Express Middleware
export function requireCompanyPermission(permissionKey: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user?.userId;
    const companyId = req.params.companyId || req.body.companyId;
    
    // Platform admins bypass all checks
    if (req.isPlatformAdmin) {
      return next();
    }
    
    const hasPermission = await hasCompanyPermission(
      userId,
      companyId,
      permissionKey
    );
    
    if (!hasPermission) {
      return res.status(403).json({
        success: false,
        error: 'Insufficient permissions'
      });
    }
    
    next();
  };
}

// Usage
router.post(
  '/companies/:companyId/projects',
  requireCompanyPermission('PROJECT:CREATE'),
  projectController.create
);

Best Practices

Permission Naming Convention

Follow this structure:
  • Use UPPERCASE letters
  • Separate resource and action with colon :
  • Use underscores for multi-word resources
  • Keep actions consistent: CREATE, READ, UPDATE, DELETE, INVITE, REMOVE, etc.
Examples
// Good
TIME_ENTRY:CREATE
TIME_ENTRY:APPROVE
CLIENT_RATE:UPDATE
REPORT:EXPORT

// Bad
CreateTimeEntry      // Not uppercase
timeentry:create    // Not uppercase
TIME-ENTRY:CREATE   // Use underscore, not hyphen

Role Design Strategy

1

Start with Defaults

Use the automatically created Owner, Admin, Manager, and Member roles
2

Create Specialized Roles

Add roles for specific job functions (Project Manager, HR Manager, Accountant)
3

Assign Permissions

Grant only the permissions needed for each role’s responsibilities
4

Test Access

Verify users can perform required actions and cannot access restricted features

Global vs Company Permissions

Global Permissions

Platform administration:
  • COMPANY:CREATE
  • USER:MANAGE_ALL
  • PERMISSION:CREATE
  • ADMIN:ACCESS

Company Permissions

Company operations:
  • MEMBER:INVITE
  • PROJECT:CREATE
  • TIME_ENTRY:APPROVE
  • REPORT:VIEW

Permission Request Workflow

Audit Trail

Track permission changes:
Example
const auditLog = {
  action: 'PERMISSION_GRANTED',
  userId: 'user-123',
  permissionId: 'perm-001',
  grantedBy: 'admin-user-id',
  grantedAt: new Date(),
  reason: 'Approved via permission request'
};

Company Setup

Create and configure companies

User Management

Manage members and roles

Time Tracking

Control access to time tracking

API Reference

View complete API documentation

Build docs developers (and LLMs) love