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

> Explore Kaneo's RBAC system: understand how it manages user roles and permissions with built-in and custom roles enforced via API middleware. Dive into the technical details of better-auth.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: deep-dive
- Published: 2026-08-30

---

**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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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.