# What Is the Purpose of `packages/permissions` in Kaneo? A Deep Dive into the Authorization Layer

> Explore the purpose of packages/permissions in Kaneo. Understand how this module defines the permission vocabulary and role hierarchy for Kaneo's authorization layer.

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

---

**`packages/permissions` defines the canonical permission vocabulary and built-in role hierarchy that powers authorization across the entire Kaneo platform.**

This centralized module serves as the single source of truth for all access control decisions in the Kaneo monorepo. By isolating permissions into their own npm package (`@kaneo/permissions`), the Kaneo team ensures that API routes, database migrations, and the web client all operate from identical, type-safe role definitions.

## Where `packages/permissions` Fits in Kaneo's Architecture

The permission system sits at the foundation of Kaneo's authorization stack. It builds upon **better-auth's** generic access control utilities while defining domain-specific rules for Kaneo's resources: `project`, `task`, `label`, and `workspace`.

The module exports four concrete roles—**viewer**, **member**, **admin**, and **owner**—each with precisely scoped permissions. These roles cascade in capability: viewers read only, members gain additional project and task permissions, admins can share and delete resources, and owners hold unrestricted access.

## Core Components of the Permission Module

### The Permission Statement Definition

In [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts), the `statement` object declares every possible action on every resource:

```typescript
// Located at: packages/permissions/src/index.ts (lines 9-15)
const statement = {
  project: ["create", "read", "update", "delete", "share"],
  task: ["create", "read", "update", "delete", "assign"],
  label: ["create", "read", "update", "delete"],
  workspace: ["create", "read", "update", "delete"],
} as const;

```

This statement object is passed to `createAccessControl()` from better-auth to instantiate the `ac` (AccessControl) instance used throughout the application.

### The Four Built-In Roles

Each role extends base organization statements and customizes permissions for Kaneo's resources. Here's how they differ:

- **viewer**: Inherits from `memberAc` (better-auth's base) but restricts project access to `["read"]` only—cannot create, update, or delete.
- **member**: Expands viewer capabilities with `["create", "read", "update"]` on projects and `["create", "read", "update", "delete", "assign"]` on tasks.
- **admin**: Grants full CRUD plus `share` permission on projects; full task permissions.
- **owner**: The super-admin role defined on the better-auth side; omitted from database seeding since it's handled at the authentication layer.

These role definitions appear in [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts) at lines 19-49.

### Default Role Seeding

Not all roles are automatically created for new workspaces. The `DEFAULT_ROLE_NAMES` array specifies which roles get seeded:

```typescript
// packages/permissions/src/index.ts (lines 55-61)
export const DEFAULT_ROLE_NAMES = ["viewer", "member", "admin"] as const;
// Note: "owner" is excluded — it's a static better-auth role

```

The `owner` role is intentionally omitted because better-auth manages it directly as a static super-admin definition.

## Converting Roles for Database Storage

The `defaultRolePayloads` helper transforms immutable role statements into mutable JSON objects suitable for persisting to the `workspace_role` table:

```typescript
// packages/permissions/src/index.ts (lines 73-84)
export const defaultRolePayloads = {
  viewer: { statements: { ...viewer.statements } },
  member: { statements: { ...member.statements } },
  admin: { statements: { ...admin.statements } },
};

```

This conversion runs during workspace creation and boot-time back-fill operations, ensuring every workspace starts with consistent permission structures.

## Practical Usage Examples

### Runtime Permission Checks in API Handlers

Use the `ac` instance to validate user capabilities before executing protected operations:

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

function canUserCreateProject(userStatements: Record<string, string[]>) {
  // Returns boolean; checks if user's accumulated statements include "create" on "project"
  return ac.can("project", "create", userStatements);
}

```

The `ac.can()` method is provided directly by better-auth's `createAccessControl` implementation.

### Seeding Default Roles for New Workspaces

When provisioning a workspace, persist the standard role hierarchy:

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

async function seedWorkspaceRoles(workspaceId: string, db: any) {
  for (const roleName of Object.keys(defaultRolePayloads) as const) {
    await db.workspace_role.create({
      data: {
        workspaceId,
        name: roleName,
        payload: defaultRolePayloads[roleName], // Mutable JSON copy
      },
    });
  }
}

```

This pattern ensures database records match the code-defined role specifications exactly.

### UI Capability Detection

The web client imports the same role objects to conditionally render controls:

```typescript
import { viewer, member, admin } from "@kaneo/permissions";

function isTaskAssignable(
  currentRole: typeof viewer | typeof member | typeof admin
) {
  // Inspect the role's statements map directly for UI decisions
  return currentRole.statements.task?.includes("assign") ?? false;
}

```

This eliminates API round-trips for permission checks and prevents flickering of disabled controls.

## Key Files in `packages/permissions`

| File | Responsibility |
|------|---------------|
| [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts) | Core definitions: `statement`, role constructors, `defaultRolePayloads`, exports |
| [`packages/permissions/package.json`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/package.json) | Publishes as `@kaneo/permissions` for cross-package imports |
| [`packages/permissions/vitest.config.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/vitest.config.ts) | Test runner configuration for the permission module |
| [`packages/permissions/src/index.test.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.test.ts) | Unit tests verifying role statements and payload conversion |

## How Kaneo Uses `packages/permissions` Across the Monorepo

The `@kaneo/permissions` package enables three critical workflows:

- **API authorization** — Express/Fastify middleware imports `ac` to gate endpoints
- **Database consistency** — Migrations and workspace creation use `defaultRolePayloads` for seed data
- **Client-side feature flags** — React components import role definitions for immediate UI state decisions

Because the package is version-locked and published internally, changes to permission logic propagate through the monorepo via standard npm dependency updates, with type checking catching inconsistencies at build time.

## Summary

- **`packages/permissions`** centralizes all authorization logic in Kaneo's monorepo as the `@kaneo/permissions` npm package
- The module defines **four built-in roles** (viewer, member, admin, owner) with cascading permission levels
- **Permission statements** for projects, tasks, labels, and workspaces are declared once in [`src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/src/index.ts) and reused everywhere
- **`defaultRolePayloads`** converts immutable role definitions into database-compatible JSON for workspace seeding
- Better-auth's **`createAccessControl`** provides the underlying engine; Kaneo's layer adds domain-specific resource rules

## Frequently Asked Questions

### What permissions does the viewer role have in Kaneo?

The **viewer** role has the most restricted permissions. According to the source code in [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts), viewers can only **`read`** projects—they cannot create, update, delete, or share them. However, viewers do retain broader permissions on tasks (`create`, `read`, `update`, `delete`, `assign`) inherited from the base `memberAc` statements. This design allows viewers to participate in task workflows while maintaining read-only access to project configuration.

### Why is the owner role excluded from DEFAULT_ROLE_NAMES?

The **owner** role is excluded because it exists as a **static super-admin definition** on the better-auth side rather than as a dynamic database record. According to the Kaneo source code, better-auth handles owner-level permissions internally; therefore, seeding it into the `workspace_role` table would create redundancy and potential synchronization issues. The three seeded roles—viewer, member, and admin—cover all workspace-level permission schemes, while owner privileges operate at the organization/auth-provider level.

### How does Kaneo convert role definitions for database storage?

Kaneo uses the **`defaultRolePayloads`** helper to transform role definitions. This function, defined at lines 73-84 of [`packages/permissions/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/packages/permissions/src/index.ts), spreads the immutable `statements` object from each role into a new mutable object. This conversion is necessary because better-auth's `createAccessControl` returns frozen statement maps, but database ORMs require plain JSON-serializable objects for insertion into the `workspace_role` table.

### Can external applications use `@kaneo/permissions`?

Yes. Because `packages/permissions` is published as a standalone npm package (`@kaneo/permissions`) with its own [`package.json`](https://github.com/usekaneo/kaneo/blob/main/package.json), external applications can declare it as a dependency. The module exports typed role definitions, the `ac` access control instance, and helper functions like `defaultRolePayloads`. However, external consumers would need to align with better-auth's access control patterns, as the `ac` instance depends on `createAccessControl` from that library.