Security Measures for Permissions in Twenty CRM: Role-Based Access Control Implementation
Twenty CRM implements a defense-in-depth permission system using role-based access control (RBAC), granular permission flags, and row-level predicates, with centralized enforcement through NestJS guards and the PermissionsService.
Twenty CRM, an open-source Salesforce alternative, employs a sophisticated security architecture to regulate access across users, API keys, and internal applications. The permission model combines role configurations with fine-grained feature toggles and object-level permissions, ensuring comprehensive data protection while maintaining flexibility for complex organizational structures.
Authentication Context and Role Resolution
The foundation of Twenty's security measures lies in determining the caller's identity and translating it into a structured permission configuration. The resolveRolePermissionConfig function in packages/twenty-server/src/engine/twenty-orm/utils/resolve-role-permission-config.util.ts handles this resolution by inspecting the authentication context and returning a RolePermissionConfig.
This configuration supports three distinct modes:
- System bypass (
shouldBypassPermissionChecks: true) for internal system operations - Intersection of roles requiring the actor to satisfy permissions from multiple roles simultaneously
- Union of roles allowing permission aggregation across multiple role assignments
// resolveRolePermissionConfig implementation
if (isSystemAuthContext(authContext)) {
return { shouldBypassPermissionChecks: true };
}
if (isApiKeyAuthContext(authContext)) {
const roleId = apiKeyRoleMap[authContext.apiKey.id];
return roleId ? { intersectionOf: [roleId] } : null;
}
if (isApplicationAuthContext(authContext) && isDefined(authContext.application.defaultRoleId)) {
return { intersectionOf: [authContext.application.defaultRoleId] };
}
if (isUserAuthContext(authContext)) {
const roleId = userWorkspaceRoleMap[authContext.userWorkspaceId];
return roleId ? { intersectionOf: [roleId] } : null;
}
The RolePermissionConfig type definition in packages/twenty-server/src/engine/twenty-orm/types/role-permission-config.ts formalizes these structures, enabling the permission system to handle complex scenarios like users with multiple roles or service accounts with elevated privileges.
Permission Flags and Feature Access
Twenty CRM utilizes fine-grained permission flags stored in packages/twenty-sdk/src/sdk/roles/permission-flag-type.ts to control feature access. These boolean toggles—such as VIEWS, API_KEYS_AND_WEBHOOKS, and IMPERSONATE—exist at both the role level (permissionFlags) and per-object level (objectsPermissions).
The PermissionsService in packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts evaluates these flags through methods like hasViewsPermission, which checks permissions differently based on the actor type:
// PermissionsService.hasViewsPermission implementation
if (isDefined(userWorkspaceId)) {
const permissions = await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
});
return permissions.permissionFlags[PermissionFlagType.VIEWS] ?? false;
}
if (isDefined(apiKeyId)) {
return this.permissionsService.userHasWorkspaceSettingPermission({
workspaceId,
apiKeyId,
setting: PermissionFlagType.VIEWS,
});
}
return false;
This dual-path evaluation ensures that API keys undergo the same rigorous permission checks as human users, preventing privilege escalation through alternative authentication methods.
Object-Level and Row-Level Security
Beyond feature flags, Twenty implements object-level permissions and row-level predicates to restrict data visibility. The computePermissionIntersection utility in packages/twenty-server/src/engine/twenty-orm/utils/compute-permission-intersection.util.ts merges permissions from multiple roles into a single effective permission set.
This utility handles:
canRead,canUpdate,canSoftDelete, andcanDestroyoperations- Field-level restrictions within objects
- Intersection logic for multi-role scenarios
When a user belongs to multiple roles, the system computes the restrictive intersection of permissions, ensuring that a user only accesses data allowed by all their assigned roles, not just one.
Enforcing Access with NestJS Guards
Twenty CRM integrates permission checks into the HTTP and GraphQL request pipeline using NestJS guards. The UpdateViewPermissionGuard in packages/twenty-server/src/engine/metadata-modules/view-permissions/guards/update-view-permission.guard.ts exemplifies this pattern:
// UpdateViewPermissionGuard.canActivate implementation
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const args = gqlContext.getArgs();
const viewId = typeof args?.id === 'string' ? args.id : request.params?.id;
return this.viewAccessService.canUserModifyView(
viewId,
request.userWorkspaceId,
request.workspace.id,
request.apiKey?.id,
);
Guards extract request context, delegate validation to specialized services, and reject unauthorized requests before they reach business logic controllers.
Special Permission Rules for View Ownership
The security measures include contextual exceptions for specific resource types. The ViewAccessService in packages/twenty-server/src/engine/metadata-modules/view-permissions/services/view-access.service.ts implements a special rule allowing view owners to modify their own UNLISTED views even without the global VIEWS permission flag:
const isOwnUnlistedView =
view.visibility === ViewVisibility.UNLISTED &&
view.createdByUserWorkspaceId === userWorkspaceId;
if (isOwnUnlistedView) {
return true;
}
This pattern demonstrates Twenty's ability to combine role-based permissions with resource ownership checks for nuanced access control.
Practical Implementation Examples
Checking View Creation Permissions
async function canCreateView(
visibility: ViewVisibility,
ctx: RequestContext,
): Promise<boolean> {
const viewAccess = new ViewAccessService(viewService, permissionsService);
return viewAccess.canUserCreateView(
visibility,
ctx.userWorkspaceId,
ctx.workspaceId,
ctx.apiKey?.id,
);
}
Retrieving Effective Object Permissions
import { PermissionsService } from '@/engine/metadata-modules/permissions/permissions.service';
async function getEffectiveObjectPermissions(
userWorkspaceId: string,
workspaceId: string,
) {
const { objectsPermissions } = await permissionsService.getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
});
// Returns a map of objectMetadataId → permission flags
return objectsPermissions;
}
Configuring Tool Permissions
import { RolePermissionConfig } from '@/engine/twenty-orm/types/role-permission-config';
// Require user to belong to BOTH role A AND role B
const config: RolePermissionConfig = {
intersectionOf: ['role-a-id', 'role-b-id'],
};
const hasToolPermission = await permissionsService.hasToolPermission(
config,
workspaceId,
PermissionFlagType.IMPORT_CSV,
);
Summary
- Role-based architecture: Twenty CRM uses
RolePermissionConfigto manage permissions through intersections, unions, or bypasses based on authentication context. - Granular flag system:
PermissionFlagTypeprovides feature-level toggles enforced by the centralizedPermissionsService. - Layered security: The system combines authentication context resolution, permission flags, object-level permissions, and row-level predicates for defense-in-depth.
- Guard integration: NestJS guards like
UpdateViewPermissionGuardenforce checks automatically at the request boundary. - Contextual exceptions: Special rules, such as unlisted view ownership, allow fine-tuned access without compromising overall security posture.
Frequently Asked Questions
How does Twenty CRM handle permissions for API keys versus regular users?
According to the source code in resolve-role-permission-config.util.ts, API keys resolve to a single role through the apiKeyRoleMap lookup, returning an intersectionOf configuration containing that role ID. The PermissionsService then evaluates API key permissions through dedicated methods like userHasWorkspaceSettingPermission, ensuring API keys undergo the same flag checks as users but without user workspace associations.
What happens when a user has multiple roles assigned?
The computePermissionIntersection utility in compute-permission-intersection.util.ts merges permissions from multiple roles by computing the intersection of capabilities. This means a user gains only the permissions shared across all their assigned roles, following the principle of least privilege. For example, if Role A allows reading Contacts but Role B does not, the user cannot read Contacts.
Can system operations bypass all permission checks?
Yes, system contexts identified by isSystemAuthContext in the role resolution utility receive a RolePermissionConfig with shouldBypassPermissionChecks: true. This allows internal system processes to perform necessary operations without restrictive permission overhead, though this bypass applies exclusively to system-level authentication contexts and never to user or API key requests.
How are row-level security predicates implemented in Twenty?
Row-level restrictions are implemented through SQL-level filters combined with object permissions via computePermissionIntersection. These predicates restrict which records a user can access based on custom conditions beyond role assignments. The system merges these row-level rules with the canRead, canUpdate, and other object-level permission flags to determine final access rights at the database query level.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →