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

> Discover how Kaneo manages user and workspace relationships with its Drizzle-ORM schema. Learn about role-based permissions and invitation flows in this technical deep dive.

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

---

**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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/require-workspace-permission.ts). This middleware implements the following logic:

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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:

```typescript
// 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`**:

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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.