How Does Kaneo Manage User and Workspace Relationships? A Technical Deep Dive into Roles and Permissions

Kaneo manages user and workspace relationships through a relational Drizzle-ORM schema that links users to workspaces via a membership table, enforces role-based permissions at the API layer using centralized middleware, and handles invitations through a dedicated pending-invitation flow.

Kaneo implements a robust, type-safe system for managing how users interact with workspaces. By treating membership as a first-class entity and enforcing authorization at the API boundary, the platform ensures secure collaboration boundaries. This article examines exactly how Kaneo structures these relationships in apps/api/src/database/schema.ts and protects them at runtime.

Core Data Model for User-Workspace Relationships

Kaneo’s architecture separates identity, collaboration boundaries, and membership into distinct tables, linked through foreign key relationships defined with Drizzle-ORM.

User Identity Storage

The userTable defined in apps/api/src/database/schema.ts (lines 23-44) stores basic identity fields including id, email, and name. This table acts as the canonical source for user existence, while workspace-specific attributes live elsewhere.

Workspace Boundaries

The workspaceTable (lines 41-51 in the same file) represents a top-level collaboration container with fields like id, name, and slug. Each workspace operates as an isolated boundary, and all permissions are scoped within this context.

Membership Linkage

The relationship between users and workspaces is mediated by the workspaceUserTable (named workspace_member in the raw schema definition at lines 53-72). This junction table contains:

  • userId and workspaceId foreign keys
  • A role column indicating the member’s capacity (e.g., member, admin)
  • Timestamps tracking when the user joined

Storing membership separately from the user or workspace tables allows Kaneo to support many-to-many relationships where a single user can belong to multiple workspaces with distinct roles.

Role-Based Access Control Implementation

Beyond simple linkage, Kaneo implements a permission system that maps workspace roles to specific capabilities.

Permission Definitions

The workspaceRoleTable (lines 85-98 in apps/api/src/database/schema.ts) maps a workspace-scoped role name to permission strings such as manage_settings, create, or update. Membership rows reference these role names, and the system resolves what actions are permitted at request time based on this mapping.

Runtime Enforcement with Middleware

Authorization is enforced by the requireWorkspacePermission utility located in apps/api/src/utils/require-workspace-permission.ts. This middleware implements the following logic:

export function requireWorkspacePermission(perm: PermissionMap) {
  return async (c, next) => {
    const user = await getUserFromSession(c);
    const member = await db
      .select()
      .from(workspaceUserTable)
      .where(and(eq(workspaceUserTable.userId, user.id), …));

    if (!hasPermission(member.role, perm)) {
      throw new HTTPException(403, { message: "Forbidden" });
    }
    return next();
  };
}

When an API endpoint is hit, the middleware reads the caller’s session from sessionTable, joins to workspace_member to fetch the user's role, and validates it against the PermissionMap imported from @kaneo/permissions. If the role lacks the requested capability, the middleware throws an HTTPException, ensuring that authorization is always enforced at the API level, not just in the UI.

Managing the Membership Lifecycle

Kaneo provides frontend abstractions to query, invite, and modify workspace members, all backed by the schema described above.

Inviting New Members

New collaborators enter the system through the invitationTable (lines 58-78 in apps/api/src/database/schema.ts), which stores pending invitations with email, role, expiresAt, and a reference to the inviter. The frontend creates these rows via the useInviteWorkspaceUser hook:

// apps/web/src/hooks/mutations/workspace-user/use-invite-workspace-user.ts
const mutation = useMutation({
  mutationFn: async (req) => {
    const { data, error } = await authClient.organization.inviteUser(req);
    if (error) throw error;
    return data;
  },
});

Once accepted, the invitation converts into a permanent row in workspaceUserTable.

Querying and Updating Members

To display current members, the frontend uses the typed client function getWorkspaceUsers:

// apps/web/src/fetchers/workspace-user/get-workspace-users.ts
export async function getWorkspaceUsers({ workspaceId }: GetWorkspaceUsersRequest) {
  return authClient.workspace.listUsers({ workspaceId });
}

Role changes are handled by the useUpdateWorkspaceUserRole mutation, which updates the role column on the workspace_member table. Subsequent requests immediately respect the new permission set because the middleware resolves roles at runtime.

Summary

  • Kaneo uses Drizzle-ORM tables in apps/api/src/database/schema.ts to define users, workspaces, and memberships with strict foreign key relationships.
  • The workspaceUserTable (named workspace_member in the schema) acts as the junction table linking users to workspaces with role assignments and join dates.
  • Permissions are enforced at the API level by the requireWorkspacePermission middleware, which validates roles against a PermissionMap from @kaneo/permissions.
  • Invitations are staged in invitationTable before converting to full memberships, preventing unauthorized access.
  • Frontend fetchers like getWorkspaceUsers and mutations like useInviteWorkspaceUser provide type-safe access to these relationships across the stack.

Frequently Asked Questions

How does Kaneo store the relationship between users and workspaces?

Kaneo stores the relationship in the workspaceUserTable (aliased as workspace_member in the database schema defined in apps/api/src/database/schema.ts). This table contains foreign keys to both the user and workspace tables, along with a role column and join date, creating a many-to-many relationship between users and workspaces.

Where does Kaneo enforce workspace permissions?

Permission enforcement happens at the API layer in the requireWorkspacePermission middleware located in apps/api/src/utils/require-workspace-permission.ts. This utility resolves the user's role from the workspace_member table and validates it against a PermissionMap from @kaneo/permissions, throwing an HTTPException if access is denied.

What tables are involved in Kaneo's invitation system?

The invitation system relies on the invitationTable defined in apps/api/src/database/schema.ts, which stores pending invites with columns for email, role, expiresAt, and the inviter's reference. Once accepted, the invitation converts into a row in the workspaceUserTable, granting the user access to the workspace.

How are roles resolved during API requests?

During each request, the requireWorkspacePermission middleware reads the user's session from sessionTable, joins to workspace_member to retrieve the assigned role, and compares that role against the required permission in the PermissionMap. This resolution happens at runtime for every protected endpoint, ensuring current permission state is always respected.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →