What Is the Purpose of `packages/permissions` in Kaneo? A Deep Dive into the Authorization Layer
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, the statement object declares every possible action on every resource:
// 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
sharepermission 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 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:
// 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:
// 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:
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:
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:
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 |
Core definitions: statement, role constructors, defaultRolePayloads, exports |
packages/permissions/package.json |
Publishes as @kaneo/permissions for cross-package imports |
packages/permissions/vitest.config.ts |
Test runner configuration for the permission module |
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
acto gate endpoints - Database consistency — Migrations and workspace creation use
defaultRolePayloadsfor 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/permissionscentralizes all authorization logic in Kaneo's monorepo as the@kaneo/permissionsnpm 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.tsand reused everywhere defaultRolePayloadsconverts immutable role definitions into database-compatible JSON for workspace seeding- Better-auth's
createAccessControlprovides 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, 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, 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, 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.
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 →