Six roles are seeded at database migration time. They are system roles (is_system_role = true) and apply across all organizations. Org admins can also create custom org-specific roles.
| Role | Description |
|---|---|
super_admin |
Platform-level admin. Full access to all orgs, all resources, all actions. |
org_admin |
Organization admin. Full access within their org. Can manage users and roles. |
hiring_manager |
Manages job requisitions, reviews candidate pipeline, views analytics. |
recruiter |
Sources candidates, manages pipeline, uses AI features. |
interviewer |
Read-only access to candidates + jobs. Can submit interview feedback. |
viewer |
Read-only access to candidates, jobs, and analytics. |
All 20 permissions in the system:
| Permission Key | Description |
|---|---|
candidates:read |
View candidate profiles and lists |
candidates:create |
Add new candidates |
candidates:update |
Edit candidate information |
candidates:delete |
Permanently delete candidates |
jobs:read |
View job requisitions |
jobs:create |
Create new job requisitions |
jobs:update |
Edit job requisitions |
jobs:delete |
Delete job requisitions |
pipeline:read |
View pipeline entries and interview rounds |
pipeline:update |
Move candidates through stages, schedule rounds, manage offers |
sourcing:run |
Start new candidate sourcing jobs |
sourcing:view |
View sourcing job history and results |
analytics:read |
View recruitment analytics and funnel data |
users:read |
View team member list |
users:manage |
Create, edit, deactivate users and assign roles |
roles:manage |
Create and edit custom roles, assign permissions |
ai:use |
Use Claude AI features (scoring, JD parsing, emails, etc.) |
settings:view |
View organization settings |
settings:manage |
Edit organization settings and API keys |
integrations:manage |
Configure third-party integrations (Keka, Gmail, etc.) |
| Permission | super_admin | org_admin | hiring_manager | recruiter | interviewer | viewer |
|---|---|---|---|---|---|---|
| candidates:read | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| candidates:create | ✅ | ✅ | ✅ | |||
| candidates:update | ✅ | ✅ | ✅ | |||
| candidates:delete | ✅ | ✅ | ||||
| jobs:read | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| jobs:create | ✅ | ✅ | ✅ | |||
| jobs:update | ✅ | ✅ | ✅ | |||
| jobs:delete | ✅ | ✅ | ||||
| pipeline:read | ✅ | ✅ | ✅ | ✅ | ✅ | |
| pipeline:update | ✅ | ✅ | ✅ | ✅ | ||
| sourcing:run | ✅ | ✅ | ✅ | |||
| sourcing:view | ✅ | ✅ | ✅ | |||
| analytics:read | ✅ | ✅ | ✅ | ✅ | ✅ | |
| users:read | ✅ | ✅ | ||||
| users:manage | ✅ | ✅ | ||||
| roles:manage | ✅ | ✅ | ||||
| ai:use | ✅ | ✅ | ✅ | ✅ | ||
| settings:view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| settings:manage | ✅ | ✅ | ||||
| integrations:manage | ✅ | ✅ |
# app/core/deps.py
def require_permission(permission: str) -> Callable:
async def _check(user: User = Depends(get_current_user)) -> User:
perms = get_user_permissions(user)
if permission not in perms:
raise HTTPException(403, f"Permission denied: '{permission}' required")
return user
return _check
@router.delete("/{candidate_id}")
async def delete_candidate(
candidate_id: uuid.UUID,
user = Depends(require_permission("candidates:delete")) # ← enforced
):
...
@router.get("/admin/users")
async def list_all_users(
user = Depends(require_role("super_admin")) # ← role check
):
...
// Hide delete button from viewers
<PermissionGate permission="candidates:delete">
<Button variant="danger" onClick={handleDelete}>Delete</Button>
</PermissionGate>
// Show admin panel only to admins
<PermissionGate role={['org_admin', 'super_admin']}>
<AdminPanel />
</PermissionGate>
// In App.tsx
<Route path="/admin" element={
<RoleGuard role={['super_admin', 'org_admin']}>
<Admin />
</RoleGuard>
} />
const { hasPermission, hasRole, isAdmin } = usePermission()
if (hasPermission('sourcing:run')) {
// Show sourcing button
}
Org admins can create custom roles via the Admin panel or API:
curl -X POST /api/v1/roles \
-H "Authorization: Bearer TOKEN" \
-d '{
"name": "Senior Recruiter",
"description": "Can do everything a recruiter does + manage settings",
"permissions": [
"candidates:read", "candidates:create", "candidates:update",
"jobs:read", "pipeline:read", "pipeline:update",
"sourcing:run", "sourcing:view", "ai:use",
"analytics:read", "settings:view", "settings:manage"
]
}'
# Create user with role
curl -X POST /api/v1/users \
-d '{"email": "john@acme.com", "name": "John", "password": "...", "role": "recruiter"}'
# Change user role
curl -X POST /api/v1/roles/assign \
-d '{"user_id": "uuid-here", "role_name": "hiring_manager"}'
/admin → Team tab → Edit user → Change Role
Every mutating API call (POST, PUT, PATCH, DELETE) that succeeds (HTTP < 400) is automatically logged to audit_logs:
{
"org_id": "...",
"user_id": "...",
"action": "DELETE",
"resource": "candidates",
"resource_id": "cand-uuid",
"ip_address": "1.2.3.4",
"created_at": "2025-01-01T00:00:00"
}
The audit log is append-only and cannot be deleted through the API.