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
memberAcwith read-only access to projects, tasks, labels, and workspaces - member: Extends
memberAcwith create/read on projects, create/read/update on tasks, full label CRUD, and workspace read access - admin: Extends
adminAcwith full CRUD on projects, tasks, and labels, plus workspace read, update, and settings management - owner: Extends
ownerAcwith 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:
- Extracts the
workspaceIdfrom the Hono request context - Checks for API key presence and validates scoped permissions immediately
- Bypasses all checks for instance admins flagged by
isInstanceAdmin - Queries the
workspace_usertable to retrieve the user’s assigned role - Resolves permission statements:
- Custom roles: Fetches the database row and parses JSON via
customRoleStatements - Built-in roles: Falls back to static definitions from
@kaneo/permissionsviabuiltInRoleStatements
- Custom roles: Fetches the database row and parses JSON via
- Validates that the role’s statements satisfy all required actions using the
satisfiesmethod
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_roletable as JSON payloads and parsed at runtime viaparsePermissionStatements - The
hasWorkspacePermissionfunction inapps/api/src/utils/require-workspace-permission.tsresolves 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
requireWorkspacePermissionmiddleware 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →