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.ts file declares a static set of statements describing what actions each resource supports. This module exports the base ac instance, the statement object, and the four built-in roles: viewer, member, admin, and owner.

  • Database Persistence: The workspace_role table defined in apps/api/src/database/schema.ts (lines 85-99) stores editable JSON payloads for the three customizable roles (viewer, member, admin) per workspace. The permission column contains a stringified JSON object that mirrors the static statements, allowing per-workspace overrides.

  • Runtime Resolution: In apps/api/src/auth.ts, Better-Auth’s createAccessControl loads the built-in role definitions, then merges any matching workspace_role row 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:

  1. Loads built-in role definitions from packages/permissions/src/index.ts
  2. Queries the workspace_role table for the user's assigned role
  3. Merges the JSON payload with base permissions
  4. 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_role table with JSON payloads that override built-in permissions
  • The three editable built-in roles (viewer, member, admin) can be customized per workspace, while owner remains static
  • Permission payloads map resources (project, task, label, workspace) to arrays of allowed actions
  • Runtime resolution merges static definitions from packages/permissions/src/index.ts with database overrides in apps/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →