How Kaneo Manages User Roles and Permissions: A Technical Deep Dive into the RBAC System

Kaneo implements a tiered, extensible RBAC model using better-auth access-control statements, supporting four built-in roles (viewer, member, admin, owner) and database-backed custom roles that are enforced via middleware on every API endpoint.

Kaneo’s permission system is built on top of better-auth and centered around access-control (AC) statements that define allowed actions on resources like projects, tasks, labels, and workspaces. This architecture provides a static baseline of roles while enabling runtime customization through persisted database records. The system is implemented across the monorepo with core logic residing in the permissions package and enforcement handled by API middleware.

Core Permission Architecture

The foundation of Kaneo’s RBAC system lives in packages/permissions/src/index.ts. This file defines a static set of permission statements and instantiates an AC object that generates four built-in roles: viewer, member, admin, and owner.

Each role extends a specific access-control base (memberAc, adminAc, or ownerAc) and inherits a curated set of actions:

  • viewer: Extends memberAc with read-only access to projects, tasks, labels, and workspaces
  • member: Extends memberAc with create/read on projects, create/read/update on tasks, full label CRUD, and workspace read access
  • admin: Extends adminAc with full CRUD on projects, tasks, and labels, plus workspace read, update, and settings management
  • owner: Extends ownerAc with the same permissions as admin plus the ability to delete workspaces

These static definitions serve as the fallback when no custom role is assigned, ensuring consistent permission semantics across all workspaces.

Database-Driven Custom Roles

While built-in roles provide a foundation, Kaneo supports dynamic permission definitions through the workspace_role table. When a workspace is created, the system seeds three editable rows (viewer, member, admin) using defaultRolePayloads, which serialize the static statements as JSON.

New custom roles can be created via the UI or API and stored in the same table. The function parsePermissionStatements (defined in apps/api/src/utils/require-workspace-permission.ts) deserializes these JSON payloads at runtime, allowing administrators to grant arbitrary combinations of permissions without deploying code changes.

Permission Enforcement Flow

Every protected API endpoint relies on a centralized lookup routine to resolve and validate permissions against the request context.

The hasWorkspacePermission Helper

The core validation logic resides in apps/api/src/utils/require-workspace-permission.ts within the hasWorkspacePermission function. This async routine executes the following resolution steps:

  1. Extracts the workspaceId from the Hono request context
  2. Checks for API key presence and validates scoped permissions immediately
  3. Bypasses all checks for instance admins flagged by isInstanceAdmin
  4. Queries the workspace_user table to retrieve the user’s assigned role
  5. Resolves permission statements:
    • Custom roles: Fetches the database row and parses JSON via customRoleStatements
    • Built-in roles: Falls back to static definitions from @kaneo/permissions via builtInRoleStatements
  6. Validates that the role’s statements satisfy all required actions using the satisfies method

If any step fails, the function returns false, triggering an HTTP error response.

Middleware Integration

API routes declare required permissions using the requireWorkspacePermission middleware. This wrapper calls hasWorkspacePermission and throws a 401 for unauthenticated requests or a 403 for insufficient permissions.

Routes attach this middleware in their definition arrays, specifying a permission map that maps resource types to arrays of required actions.

Implementing Permission Checks in Practice

Developers interact with the system through declarative middleware or programmatic checks.

To protect an endpoint, import the middleware and specify the resource and action:

// apps/api/src/tasks/routes/create-task.ts
export const createTask = createRoute({
  method: "post",
  path: "/tasks",
  middleware: [
    requireWorkspacePermission({ task: ["create"] }),
  ],
  // ...validation and handler
});

For conditional logic inside services, use the helper directly:

// Programmatic permission check
import { hasWorkspacePermission } from "@kaneo/api/src/utils/require-workspace-permission";

async function canDeleteProject(c: Context) {
  return await hasWorkspacePermission(c, { project: ["delete"] });
}

To create a custom role programmatically, insert a row with a JSON permission payload:

// Creating a custom "support" role
await db.insert(schema.workspaceRoleTable).values({
  workspaceId: "w_123",
  role: "support",
  permission: JSON.stringify({
    task: ["read", "update"],
    label: ["read"],
    project: ["read"],
  }),
});

Summary

  • Kaneo’s RBAC system is built on better-auth access-control statements defined in packages/permissions/src/index.ts
  • Four built-in roles (viewer, member, admin, owner) provide static permission baselines with escalating privileges
  • Custom roles are stored in the workspace_role table as JSON payloads and parsed at runtime via parsePermissionStatements
  • The hasWorkspacePermission function in apps/api/src/utils/require-workspace-permission.ts resolves roles from either the database or static definitions
  • Instance admins bypass all permission checks, while API keys are validated against scoped permissions before role resolution
  • The requireWorkspacePermission middleware enforces permissions on API endpoints, returning 401 or 403 status codes for unauthorized access

Frequently Asked Questions

Can I modify the built-in viewer, member, or admin roles in Kaneo?

Yes. The constant DEFAULT_ROLE_NAMES includes "viewer", "member", and "admin", which are seeded as editable rows in the workspace_role table when a workspace is created. You can modify these through the UI or API, though the owner role remains a static better-auth role that cannot be customized or deleted.

How does Kaneo handle permission checks for API keys?

API keys are checked before user role resolution in the hasWorkspacePermission function. If a request includes an API key, the system validates that the key’s scoped permissions match the required actions. If the API key lacks the necessary permissions, the request is denied immediately without checking the user's workspace role.

What happens if a user has a custom role that isn't found in the database?

If a user is assigned a custom role that no longer exists in the workspace_role table, the permission lookup will fail to find a matching row. In this scenario, the system may deny access or fall back to default behavior depending on the implementation, though the code typically expects valid role assignments stored in the workspace_user table to reference existing custom or built-in roles.

How can I programmatically check permissions outside of middleware?

Import the hasWorkspacePermission utility from apps/api/src/utils/require-workspace-permission.ts and pass it the Hono context along with a permission map. This returns a boolean indicating whether the current request context satisfies the required permissions, allowing you to use it in service methods or conditional business logic.

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 →