# How Permissions Are Defined and Managed in Kaneo: A Complete Technical Guide

> Discover how Kaneo defines and manages permissions using a declarative access control system and a centralized permission map. Explore role definitions and middleware enforcement.

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

---

**Kaneo implements a declarative access-control system built on a centralized permission map that extends better-auth statements, storing role definitions as JSON in the `workspace_role` table and enforcing them via the `requireWorkspacePermission` middleware.**

Kaneo is an open-source project management platform that uses a granular, resource-based permission system to control access to projects, tasks, labels, and workspace settings. This article examines how permissions are defined and managed in Kaneo, covering the declarative permission map, role storage mechanisms, and enforcement middleware implemented across the TypeScript codebase.

## The Declarative Permission Map

The foundation of Kaneo's access control lives in [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts). This file exports a **`statement`** constant that extends the default statements from *better-auth* with Kaneo-specific resources and actions.

```typescript
export const statement = {
  ...defaultStatements,
  project: ["create", "read", "update", "delete", "share"],
  task:    ["create", "read", "update", "delete", "assign"],
  label:   ["create", "read", "update", "delete"],
  workspace: ["read", "update", "delete", "manage_settings"],
} as const;

```

This map defines every possible action on each core resource. A **Role** is created from this map via `createAccessControl(statement)`. The system provides four built-in roles—**viewer**, **member**, **admin**, and **owner**—each receiving a subset of these actions (defined in lines 19‑49 of the file).

The role definitions are exported as `builtInRoles`, while the standard role names are listed in `DEFAULT_ROLE_NAMES`.

## Database Storage and Default Payloads

Permissions for a workspace are persisted in the **`workspace_role`** table as a JSON string that mirrors the role's `.statements`. The payloads are generated by **`toMutablePayload`** (lines 63‑70) and exposed as **`defaultRolePayloads`** (lines 73‑84).

When a workspace is created, [`apps/api/src/utils/seed-default-workspace-roles.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/seed-default-workspace-roles.ts) inserts rows for the default roles using `defaultRolePayloads`. This ensures the database row matches the static role definition while still allowing UI-based edits later.

## Enforcing Permissions with Middleware

The API layer enforces permissions through the **`requireWorkspacePermission`** middleware located in [`apps/api/src/utils/require-workspace-permission.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/require-workspace-permission.ts):

```typescript
export function requireWorkspacePermission(permissions: PermissionMap) {
  return async (c, next) => {
    const has = await hasWorkspacePermission(c, permissions);
    if (!has) throw new HTTPException(403, { message: "Insufficient permissions" });
    await next();
  };
}

```

The helper `hasWorkspacePermission` reads the `workspace_role.permission` column, parses it with `parsePermissionStatements`, and uses the **`satisfies`** routine from `better-auth` to verify that the stored actions include the required ones.

## Applying Permissions in API Routes

Controllers invoke the middleware to protect endpoints. For example, to restrict project creation to users with the appropriate permission:

```typescript
import { requireWorkspacePermission } from "../utils/require-workspace-permission";

router.post(
  "/project",
  requireWorkspacePermission({ project: ["create"] }),
  createProjectController,
);

```

This guarantees that only users whose role grants `project:create` can access the endpoint. Similarly, task deletion can be protected:

```typescript
import { requireWorkspacePermission } from "@/utils/require-workspace-permission";

router.delete(
  "/task/:taskId",
  requireWorkspacePermission({ task: ["delete"] }),
  async (c) => { /* delete logic */ }
);

```

## Extending the System with Custom Roles

Custom roles can be added without modifying the core codebase by inserting a new row in `workspace_role` with a JSON payload following the same shape (`{ resource: ["action", …] }`). The existing `requireWorkspacePermission` logic automatically respects these new roles.

To create a custom role from the client side:

```typescript
import { api } from "@kaneo/client";

await api.mutate({
  endpoint: "/workspace/:id/role",
  method: "POST",
  body: {
    name: "qa_engineer",
    permission: {
      project: ["read"],
      task: ["read", "update"],
      label: ["read"],
      workspace: ["read"],
    },
  },
});

```

For debugging, you can inspect a role's statements using the exported constants:

```typescript
import { builtInRoles } from "@kaneo/permissions";

console.log(builtInRoles.admin.statements);
/*
{
  project: ["create","read","update","delete","share"],
  task:    ["create","read","update","delete","assign"],
  label:   ["create","read","update","delete"],
  workspace: ["read","update","manage_settings"],
  // plus default organization statements …
}
*/

```

## Key Files in the Permission System

| File | Purpose |
|------|---------|
| [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts) | Defines the permission map, creates the built‑in roles, and exports default payloads |
| [`apps/api/src/utils/require-workspace-permission.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/require-workspace-permission.ts) | Middleware that enforces permissions on API routes |
| [`apps/api/src/utils/seed-default-workspace-roles.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/seed-default-workspace-roles.ts) | Seeds the database with default role rows on workspace creation |
| [`apps/web/src/lib/permissions.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/permissions.ts) | Re‑exports the role constants for the front‑end UI |
| [`apps/web/src/hooks/use-workspace-permission.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/use-workspace-permission.ts) | Client‑side hook that queries the server’s permission check endpoint |

## Summary

- **Permissions are defined declaratively** in [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts) via a statement map extending better-auth with Kaneo-specific resources like `project`, `task`, and `workspace`
- **Four built-in roles** (viewer, member, admin, owner) are generated using `createAccessControl` and stored in the `workspace_role` table as JSON via `toMutablePayload`
- **Automatic seeding** occurs during workspace creation through [`seed-default-workspace-roles.ts`](https://github.com/usekaneo/kaneo/blob/main/seed-default-workspace-roles.ts), ensuring database consistency with static definitions
- **Server-side enforcement** happens through the `requireWorkspacePermission` middleware, which uses `hasWorkspacePermission` and the `satisfies` routine to validate requests against stored JSON permissions
- **Custom roles** can be added dynamically by inserting rows with valid JSON payloads, extending the system without code changes

## Frequently Asked Questions

### Where are permissions defined in the Kaneo codebase?

Permissions are defined in [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts) as a declarative `statement` object that extends better-auth default statements. This file exports the permission map, built-in roles created via `createAccessControl`, and default payloads for database seeding.

### How does Kaneo store and retrieve user permissions?

Kaneo stores permissions in the `workspace_role` table as JSON strings matching the role's `.statements` structure. The `hasWorkspacePermission` utility retrieves and parses this JSON using `parsePermissionStatements`, then validates it against required permissions using better-auth's `satisfies` routine.

### Can I create custom roles in Kaneo without modifying the source code?

Yes, you can create custom roles by inserting new rows into the `workspace_role` table with a JSON payload following the shape `{ resource: ["action", ...] }`. The `requireWorkspacePermission` middleware automatically respects these custom roles, allowing extensibility without redeploying the application.

### How do I check permissions inside an API route handler?

Use the `requireWorkspacePermission` middleware from [`apps/api/src/utils/require-workspace-permission.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/require-workspace-permission.ts). Import it and apply it to your route before the controller function, passing the required permission map (e.g., `{ task: ["delete"] }`) to enforce access control at the endpoint level.