How to Configure Custom Workspace Roles in Kaneo: A Complete RBAC Guide
Kaneo implements custom workspace roles through a three-layer RBAC system that combines static permission definitions, JSON-stored database overrides, and runtime resolution via Better-Auth to let you define granular access controls per workspace.
Kaneo’s role-based access control (RBAC) allows workspace administrators to move beyond the four built-in roles and define precise permissions for projects, tasks, labels, and workspace settings. By leveraging the workspace_role table and Better-Auth’s dynamic access control as implemented in the usekaneo/kaneo repository, you can create, assign, and modify custom roles without deploying code changes.
Understanding Kaneo's Three-Layer RBAC Architecture
Kaneo’s permission system operates through three distinct layers that work together at runtime:
-
Permission Definitions: The
packages/permissions/src/index.tsfile declares a static set of statements describing what actions each resource supports. This module exports the baseacinstance, thestatementobject, and the four built-in roles:viewer,member,admin, andowner. -
Database Persistence: The
workspace_roletable defined inapps/api/src/database/schema.ts(lines 85-99) stores editable JSON payloads for the three customizable roles (viewer,member,admin) per workspace. Thepermissioncolumn contains a stringified JSON object that mirrors the static statements, allowing per-workspace overrides. -
Runtime Resolution: In
apps/api/src/auth.ts, Better-Auth’screateAccessControlloads the built-in role definitions, then merges any matchingworkspace_rolerow to produce the effective permission set for the current user.
Additionally, apps/api/src/utils/seed-default-workspace-roles.ts ensures every workspace receives default role rows on API startup, backfilling existing workspaces that lack them.
Creating a Custom Workspace Role
To define a custom role, you must construct a permission payload and persist it through the API.
Step 1: Define the Permission Payload
Create a plain object where keys represent resource names (project, task, label, workspace) and values are arrays of allowed actions:
const customPayload = {
project: ["create", "read", "update"],
task: ["create", "read", "update", "assign"],
label: ["read"],
workspace: ["read", "update"],
};
Step 2: Persist the Role via API
Insert a row into the workspace_role table using the typed client from @kaneo/libs. The endpoint follows the standard CRUD pattern:
import { client } from "@kaneo/libs";
const workspaceId = "w_01ABC...";
const roleName = "project-manager";
const payload = {
project: ["create", "read", "update", "delete", "share"],
task: ["create", "read", "update", "assign"],
label: ["create", "read", "update", "delete"],
workspace: ["read", "update"],
};
await client.post(`/workspaces/${workspaceId}/roles`, {
role: roleName,
permission: JSON.stringify(payload),
});
The role column stores your custom identifier (e.g., project-manager), while the permission column stores the JSON stringified payload.
Assigning Custom Roles to Workspace Members
Once created, apply the custom role to users by updating their workspace_member record. The role field references any name existing in the workspace_role table:
await client.patch(
`/workspaces/${workspaceId}/members/${userId}`,
{ role: roleName }
);
Better-Auth’s middleware validates permissions against this assigned role on every subsequent request.
Modifying Custom Workspace Role Permissions
Update existing roles by patching the workspace_role row with a new JSON payload. Changes take effect immediately without requiring users to re-authenticate:
const newPayload = {
...payload,
task: [...payload.task, "delete"], // add delete permission on tasks
};
await client.patch(
`/workspaces/${workspaceId}/roles/${roleName}`,
{ permission: JSON.stringify(newPayload) }
);
Runtime Enforcement and Permission Validation
When a request hits a protected route, the requireWorkspacePermission middleware resolves the effective permission set:
- Loads built-in role definitions from
packages/permissions/src/index.ts - Queries the
workspace_roletable for the user's assigned role - Merges the JSON payload with base permissions
- Validates the requested action against the final statement set
If the permission is missing, the middleware returns a 403 Forbidden response. The integration tests in tests/api-integration/workspace-rbac.test.ts verify this behavior, including malformed payload handling:
// Example test verification from workspace-rbac.test.ts
it("returns 403 when the workspace_role permission JSON is malformed", async () => {
// Test implementation verifies error handling
});
Summary
- Kaneo stores custom workspace roles in the
workspace_roletable with JSON payloads that override built-in permissions - The three editable built-in roles (
viewer,member,admin) can be customized per workspace, whileownerremains static - Permission payloads map resources (
project,task,label,workspace) to arrays of allowed actions - Runtime resolution merges static definitions from
packages/permissions/src/index.tswith database overrides inapps/api/src/auth.ts - Changes to role permissions apply immediately to all assigned members via standard PATCH requests
Frequently Asked Questions
What built-in roles come with Kaneo by default?
Kaneo ships with four built-in roles defined in packages/permissions/src/index.ts: viewer (read-only), member (standard participation), admin (management capabilities), and owner (full control). The system seeds editable database rows for viewer, member, and admin, allowing per-workspace customization, while owner remains a static super-admin role.
Can I create entirely new role names beyond the built-in ones?
Yes. The workspace_role table accepts any string identifier for the role column. When you POST to /workspaces/{workspaceId}/roles with a unique name like project-manager or external-contractor, Kaneo creates a new role definition that can be assigned to members via the membership API.
How does Kaneo handle missing or malformed permission JSON?
If the permission column contains invalid JSON or lacks required fields, Better-Auth’s runtime resolution falls back to built-in defaults for that role level. The integration tests in tests/api-integration/workspace-rbac.test.ts specifically verify that malformed payloads trigger 403 responses rather than server errors, maintaining security boundaries.
Do I need to restart the API after modifying role permissions?
No. Because Kaneo resolves permissions at request-time by querying the workspace_role table, changes to custom workspace roles take effect immediately. The seedDefaultWorkspaceRoles() function in apps/api/src/utils/seed-default-workspace-roles.ts only runs at startup to ensure default rows exist, but does not cache permission content.
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 →