Release Notes — April 10, 2026
This release completes the Custom Roles (RBAC) tier gate implementation and includes a database fix for a workspace roles index collision that prevented workspace roles from appearing in newly created workspaces.
Custom Roles — Enterprise Only (RBAC Tier Gate)
Overview
Creating, editing, and deleting custom permission roles is now gated behind the Enterprise tier. This is the industry standard: Clockify restricts custom roles to their Enterprise plan, Toggl Track restricts advanced roles to Premium+.
Default roles (Viewer, Member, Manager, Owner) remain available on all tiers with no change.
Backend
requireFeature('customRoles') guard on workspace role mutation endpoints:
| Endpoint | Guard |
|---|---|
POST /api/v1/workspace-roles | requireFeature('customRoles') |
PUT /api/v1/workspace-roles/:roleId | requireFeature('customRoles') |
DELETE /api/v1/workspace-roles/:roleId | requireFeature('customRoles') |
Non-Enterprise workspaces receive 403 FEATURE_NOT_AVAILABLE on any of these endpoints.
New FeatureName: customRoles added to the FeatureName union type in tier.types.ts.
FeatureGate.service.ts mapping: customRoles → { category: 'team', key: 'departments' } — departments is true only on the Enterprise tier, making customRoles an Enterprise-only feature.
Frontend
useFeatureAccess.ts: customRoles mapped to 'enterprise' in both feature requirement maps.
UnifiedRolesPage.tsx: The New Role button is now wrapped in <FeatureGate feature="customRoles">. On non-Enterprise workspaces, the button is replaced by an UpgradePrompt component.
Pricing Comparison Page
/api/v1/pricing-comparison now returns a custom-roles row in the Team & Collaboration category:
| FREE | PRO | ENTERPRISE | |
|---|---|---|---|
| Custom roles |
Fix: Workspace Roles Index Collision
Problem
Workspace roles (scope: workspace) have unitId: null because they are not tied to a specific workspace. The unique index { unitId, scope, name } caused an E11000 DuplicateKey error from the second workspace onwards, since all workspace roles shared unitId: null.
Effect: The /api/v1/workspace-roles?scope=workspace endpoint returned an empty list for any workspace beyond the first.
Fix
UnifiedRole.model.ts:
unitIdfield:required: false, default: null- Old single unique index removed
- Two partial unique indexes added:
{ unitId, scope, name }— only forscope: 'unit'entries{ workspaceId, scope, name }— only forscope: 'workspace'entries
roleCopy.service.ts:
copyWorkspaceRoles():insertOne→findOneAndUpdate + upsert(idempotent)copyWorkspacePermissions(): same pattern
Migration script fix-workspace-roles-index.ts (run once):
- Dropped broken index on all 25 unit DBs
- Created the two correct partial indexes
- Backfilled missing workspace roles for all workspaces
Change Summary
| Layer | Change |
|---|---|
Backend tier.types.ts | customRoles added to FeatureName union |
Backend FeatureGate.service.ts | customRoles → team.departments in both feature maps |
Backend workspaceRoles.routes.ts | requireFeature('customRoles') on POST / PUT / DELETE |
Backend UnifiedRole.model.ts | unitId: required: false + 2 partial unique indexes |
Backend roleCopy.service.ts | insertOne → findOneAndUpdate + upsert (idempotent) |
Backend pricing.controller.ts | custom-roles row added to Team & Collaboration category |
Backend stripe.config.ts | 'Custom roles (RBAC)' added to Enterprise features list |
Frontend useFeatureAccess.ts | customRoles: 'enterprise' in both feature requirement maps |
Frontend tier.types.ts | customRoles comment updated |
Frontend UnifiedRolesPage.tsx | New Role button wrapped in <FeatureGate feature="customRoles"> |
Docs subscription-plans.md | Custom roles row added to Feature Comparison Matrix |
Docs subscription-plans.md | Custom roles (RBAC) added to Enterprise features list |
Docs workspace-roles.md | Custom Roles section updated: ENT-only notice + correct UI steps |
Availability: Custom role create/edit/delete — Enterprise only. Default roles — all tiers.