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:
userIdandworkspaceIdforeign keys- A
rolecolumn 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.tsto define users, workspaces, and memberships with strict foreign key relationships. - The
workspaceUserTable(namedworkspace_memberin 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
requireWorkspacePermissionmiddleware, which validates roles against a PermissionMap from@kaneo/permissions. - Invitations are staged in
invitationTablebefore converting to full memberships, preventing unauthorized access. - Frontend fetchers like
getWorkspaceUsersand mutations likeuseInviteWorkspaceUserprovide 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →