ClickUp Enterprise RBAC
Overview
Enterprise access patterns for ClickUp API v2. ClickUp's role system is built into the workspace, and the API surfaces roles via member objects. OAuth 2.0 enables multi-workspace apps where each user authorizes their own workspaces.
ClickUp Role Model
ClickUp workspace members have role IDs in the API:
| Role ID | Role | Permissions | |---------|------|-------------| | 1 | Owner | Full control, billing, workspace settings | | 2 | Admin | Manage members, spaces, integrations | | 3 | Member | Create/edit tasks, spaces (per permission) | | 4 | Guest | Limited access to shared items only |
// Get workspace members with roles
async function getWorkspaceMembers(teamId: string) {
const data = await clickupRequest(`/team/${teamId}`);
return data.team.members.map((m: any) => ({
userId: m.user.id,
username: m.user.username,
email: m.user.email,
role: m.user.role, // 1=owner, 2=admin, 3=member, 4=guest
roleLabel: { 1: 'owner', 2: 'admin', 3: 'member', 4: 'guest' }[m.user.role],
}));
}
// Check if user can perform admin operations
function canAdminister(member: { role: number }): boolean {
return member.role <= 2; // Owner or Admin
}
OAuth 2.0 Multi-Workspace App
Build apps that access multiple ClickUp workspaces on behalf of users.
// Step 1: Redirect user to ClickUp authorization
function getOAuthUrl(state: string): string {
return `https://app.clickup.com/api?client_id=${process.env.CLICKUP_CLIENT_ID}&redirect_uri=${encodeURIComponent(process.env.CLICKUP_REDIRECT_URI!)}&state=${state}`;
}
// Step 2: Exchange code for token
async function handleOAuthCallback(code: string) {
const response = await fetch('https://api.clickup.com/api/v2/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.CLICKUP_CLIENT_ID,
client_secret: process.env.CLICKUP_CLIENT_SECRET,
code,
}),
});
const { access_token } = await response.json();
// Step 3: Discover which workspaces user authorized
const teamsResponse = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': access_token },
});
const { teams } = await teamsResponse.json();
return {
token: access_token, // Doesn't expire (but can be revoked)
workspaces: teams.map((t: any) => ({ id: t.id, name: t.name })),
};
}
// Step 4: Store per-user tokens
interface UserClickUpAuth {
userId: string;
clickupToken: string; // Encrypt at rest
authorizedWorkspaces: string[];
connectedAt: Date;
}
Permission Middleware
// Express middleware that checks ClickUp workspace access
function requireClickUpAccess(requiredRole: number = 3) {
return async (req: any, res: any, next: any) => {
const userToken = req.user.clickupToken;
const teamId = req.params.teamId || req.body.teamId;
if (!userToken) {
return res.status(401).json({ error: 'ClickUp not connected' });
}
// Verify user still has access to this workspace
const teamsRes = await fetch('https://api.clickup.com/api/v2/team', {
headers: { 'Authorization': userToken },
});
if (!teamsRes.ok) {
return res.status(401).json({ error: 'ClickUp token expired or revoked' });
}
const { teams } = await teamsRes.json();
const workspace = teams.find((t: any) => t.id === teamId);
if (!workspace) {
return res.status(403).json({ error: 'No access to this ClickUp workspace' });
}
// Check role level
const userMember = workspace.members.find(
(m: any) => m.user.id === req.user.clickupUserId
);
if (!userMember || userMember.user.role > requiredRole) {
return res.status(403).json({
error: `Requires role ${requiredRole} or higher`,
});
}
req.clickupWorkspace = workspace;
next();
};
}
// Usage
app.delete('/api/clickup/:teamId/space/:spaceId',
requireClickUpAccess(2), // Admin required
async (req, res) => { /* ... */ }
);
User Groups (API v2 "Teams")
GET /api/v2/group Get User Groups
POST /api/v2/team/{team_id}/group Create User Group
PUT /api/v2/group/{group_id} Update User Group
DELETE /api/v2/group/{group_id} Delete User Group
// Create a user group for engineering team
await clickupRequest(`/team/${teamId}/group`, {
method: 'POST',
body: JSON.stringify({
name: 'Engineering',
member_ids: [183, 456, 789],
}),
});
Audit Trail
interface ClickUpAuditEntry {
timestamp: string;
userId: number;
workspaceId: string;
action: string;
resource: string;
resourceId: string;
success: boolean;
}
function logClickUpAction(entry: Omit<ClickUpAuditEntry, 'timestamp'>): void {
const log: ClickUpAuditEntry = {
...entry,
timestamp: new Date().toISOString(),
};
console.log(JSON.stringify({ level: 'audit', service: 'clickup', ...log }));
}
Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| OAUTH_023/027 | Workspace not authorized | User must re-authorize via OAuth flow |
| Role check fails | User role changed in ClickUp | Re-fetch member data from API |
| Token revoked | User disconnected app | Handle 401, prompt re-auth |
| Guest access denied | Endpoint requires member+ | Check role field before API call |
Resources
Next Steps
For major migrations, see clickup-migration-deep-dive.