> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.clinikally.work/llms.txt
> Use this file to discover all available pages before exploring further.

# RBAC flow overview

> How role-based access control is implemented in Medusa, including permissions, roles, caching, and middleware checks.

# This document explains how RBAC is implemented in this Medusa.

***

## Overview

This project uses a custom RBAC module to control admin-side access.

**At a high level:**

* `Permission` defines a `resource + action` pair
* `Role` is a named collection of permissions
* Users are linked to roles through a `user_role` link table
* Access checks are done through middleware before protected admin routes run
* User permissions are cached in Redis

The implementation is centered around `src/modules/rbac/service.ts`.

```mermaid theme={null}
flowchart LR
    U[User] -->|user_role link| R[Role]
    R -->|role_permission link| P[Permission]
    P -->|resource + action| Access[Access decision]
```

***

## Where the RBAC code lives

**Core module**

* `src/modules/rbac/index.ts`
* `src/modules/rbac/service.ts`
* `src/modules/rbac/models/role.ts`
* `src/modules/rbac/models/permission.ts`
* `src/modules/rbac/migrations/Migration20260421114704.ts`

**User-role linking**

* `src/links/user-role.ts`

**Route protection**

* `src/api/admin/rbac-middleware.ts`
* `src/api/middlewares.ts`

**Admin RBAC APIs**

* `src/api/admin/rbac/roles/route.ts`
* `src/api/admin/rbac/roles/[id]/route.ts`
* `src/api/admin/rbac/roles/[id]/permissions/route.ts`
* `src/api/admin/rbac/users/[id]/roles/route.ts`
* `src/api/admin/rbac/me/permissions/route.ts`
* `src/api/admin/rbac/utils.ts`

**Bootstrap / automation**

* `src/scripts/seed-rbac.ts`
* `src/subscribers/user-created-rbac.ts`

**Cache helpers**

* `src/utils/cache.ts`

***

## Data model

### Entity relationship diagram

```mermaid theme={null}
erDiagram
    USER ||--o{ USER_ROLE : "linked via"
    USER_ROLE }o--|| ROLE : "resolves to"
    ROLE ||--o{ ROLE_PERMISSION : "grants"
    ROLE_PERMISSION }o--|| PERMISSION : "references"

    USER {
        string id
    }
    ROLE {
        string id
        string name
        string display_name
        string description
        boolean is_active
    }
    PERMISSION {
        string id
        string resource
        string action
        string description
    }
```

### Permission

Defined in `src/modules/rbac/models/permission.ts:4`.

**Fields:** `id`, `resource`, `action`, `description`

**Constraints:**

* `resource + action` is unique for non-deleted rows — `src/modules/rbac/models/permission.ts:12`

**Example permissions:**

| Resource         | Action   | Combined              |
| ---------------- | -------- | --------------------- |
| `catalog`        | `view`   | `catalog.view`        |
| `orders`         | `manage` | `orders.manage`       |
| `pricing`        | `admin`  | `pricing.admin`       |
| `customer.email` | `view`   | `customer.email.view` |

### Role

Defined in `src/modules/rbac/models/role.ts:4`.

**Fields:** `id`, `name`, `display_name`, `description`, `is_active`, `permissions`

**Constraints:**

* `name` is unique for non-deleted rows — `src/modules/rbac/models/role.ts:16`
* Inactive roles are ignored during permission resolution — `src/modules/rbac/service.ts:140`

### Relationships

**Role ↔ Permission** — many-to-many

* Model relation: `src/modules/rbac/models/role.ts:10`
* Pivot table `role_permission`: `src/modules/rbac/models/role.ts:12`
* DB migration: `src/modules/rbac/migrations/Migration20260421114704.ts:19`

**User ↔ Role** — Medusa link

* Defined in `src/links/user-role.ts:5`
* Stored in table `user_role` — `src/links/user-role.ts:16`

***

## How permissions work

The permission format is built as `resource.action` — `src/modules/rbac/service.ts:56`.

**Supported match patterns**

| Pattern           | Meaning                          |
| ----------------- | -------------------------------- |
| `*`               | Full access                      |
| `resource.action` | Exact match                      |
| `resource.*`      | All actions for one resource     |
| `*.action`        | Same action across all resources |

Matching logic is implemented in `src/modules/rbac/service.ts:93`.

### Example matches

```mermaid theme={null}
flowchart TD
    A["User has: catalog.manage"] --> A1["Can do: catalog.manage only"]
    B["User has: catalog.*"] --> B1["Can do: catalog.view, catalog.manage, catalog.admin ..."]
    C["User has: *.view"] --> C1["Can do: any resource's view action"]
    D["User has: *"] --> D1["Can do: everything"]
```

| Granted permission | Requesting `orders.manage` | Requesting `orders.view` | Requesting `pricing.admin`     |
| ------------------ | -------------------------- | ------------------------ | ------------------------------ |
| `orders.manage`    | ✅                          | ❌                        | ❌                              |
| `orders.*`         | ✅                          | ✅                        | ❌                              |
| `*.manage`         | ✅                          | ❌                        | ❌ (needs `admin` not `manage`) |
| `*`                | ✅                          | ✅                        | ✅                              |

***

## How user access is resolved

```mermaid theme={null}
flowchart TD
    A[Request hits protected route] --> B[getUserRoles: load linked roles via graph query]
    B --> C{Any role is is_active === false?}
    C -->|Yes| D[Excluded from result]
    C -->|No| E[Included]
    D --> F[fetchUserPermissionsFromDb: flatten permissions from active roles]
    E --> F
    F --> G{Any role named super_admin?}
    G -->|Yes| H["Permissions = ['*']"]
    G -->|No| I[Dedupe permissions with Set]
    H --> J[checkPermission: evaluate match rules]
    I --> J
    J --> K{Match found?}
    K -->|Yes| L[Allow]
    K -->|No| M[Deny]
```

### 1. Fetch user roles

`getUserRoles` loads a user's linked roles through a Medusa graph query — `src/modules/rbac/service.ts:110`.

* Only active roles are returned — `src/modules/rbac/service.ts:140`
* Permissions can optionally be included — `src/modules/rbac/service.ts:125`

### 2. Convert roles to permission strings

`fetchUserPermissionsFromDb` flattens all permissions from all active roles — `src/modules/rbac/service.ts:159`.

* Duplicate permissions are removed with `Set` — `src/modules/rbac/service.ts:181`
* If any role is named `super_admin`, the user gets  immediately — `src/modules/rbac/service.ts:171`

### 3. Check a requested permission

`checkPermission` delegates to `getUserPermissions` and then evaluates match rules — `src/modules/rbac/service.ts:184`.

***

## Caching and invalidation

User permissions are cached in Redis.

**Cache key format** — defined in `src/utils/cache.ts:6`

```
rbac:v{generation}:user:{userId}:permissions
```

### Read flow

```mermaid theme={null}
flowchart TD
    A[getUserPermissions called] --> B[Read current RBAC generation]
    B --> C["Build key: rbac:v{gen}:user:{id}:permissions"]
    C --> D{Cache hit?}
    D -->|Yes| E[Return cached permissions]
    D -->|No / parse fails| F[Fetch from DB]
    F --> G[Write to cache, TTL 1 hour]
    G --> E
```

* Reads current RBAC generation — `src/modules/rbac/service.ts:147`
* Builds user cache key — `:148`
* Returns cached permissions if present — `:149`
* Falls back to DB if cache read/parsing fails — `:154`
* TTL is 1 hour — `:152`

### Invalidation rules

```mermaid theme={null}
sequenceDiagram
    participant Admin
    participant Service as RBAC Service
    participant Redis

    Note over Admin,Redis: Case 1 — Role assigned/removed for one user
    Admin->>Service: assignRoleToUser / removeRoleFromUser
    Service->>Redis: Invalidate that user's cache key only

    Note over Admin,Redis: Case 2 — Role's permissions changed
    Admin->>Service: assignPermissionsToRole
    Service->>Redis: Increment global RBAC generation
    Redis-->>Service: All old-generation keys now "invisible"
    Note right of Redis: No per-user deletes needed —<br/>next read builds a new-gen key
```

| Event                         | Behavior                      | Location                                                   |
| ----------------------------- | ----------------------------- | ---------------------------------------------------------- |
| Role assigned to a user       | User cache invalidated        | `src/modules/rbac/service.ts:216`                          |
| Role removed from a user      | User cache invalidated        | `src/modules/rbac/service.ts:233`                          |
| Permissions changed on a role | Global generation incremented | `src/modules/rbac/service.ts:264`, `src/utils/cache.ts:68` |

Role-permission changes invalidate all users logically by bumping the generation number, without deleting every key one by one.

***

## How routes are protected

RBAC route checks are handled by middleware in `src/api/admin/rbac-middleware.ts`.

### Request flow through middleware

```mermaid theme={null}
flowchart TD
    A[Incoming admin request] --> B[Read auth_context]
    B --> C{actor type?}
    C -->|api_key / api-key| D[Allow automatically]
    C -->|missing user id| E[401 Unauthorized]
    C -->|other than 'user'| F[403 Forbidden]
    C -->|user| G[rbacService.checkPermission]
    G --> H{Permission match?}
    H -->|Yes| I[Continue to route handler]
    H -->|No| J[403 Forbidden]
```

**Middleware helpers**

* `requirePermission(resource, action)` — `src/api/admin/rbac-middleware.ts:80`
* `requireAnyPermission(...permissions)` — `src/api/admin/rbac-middleware.ts:108`

**Auth behavior** — middleware reads `auth_context` (`:18`)

* `api_key` / `api-key` actors are automatically allowed — `:37`
* Missing user ID returns `401` — `:41`
* Actor type other than `user` returns `403` — `:45`
* Normal admin users are checked through `rbacService.checkPermission` — `:53`

**Where middleware is wired** — instances created in `src/api/middlewares.ts:50`

| Instance                                | Location                    |
| --------------------------------------- | --------------------------- |
| `accessView`                            | `src/api/middlewares.ts:50` |
| `catalogManage`                         | `src/api/middlewares.ts:60` |
| `ordersAdmin`                           | `src/api/middlewares.ts:67` |
| `pricingAdmin`                          | `src/api/middlewares.ts:73` |
| Composed `requireAnyPermission` example | `src/api/middlewares.ts:52` |

***

## RBAC admin APIs

### Roles list and create

`src/api/admin/rbac/roles/route.ts`

* `GET /admin/rbac/roles` — supports `limit`, `offset`, `q`; loads role permissions too (`:23`)
* `POST /admin/rbac/roles` — creates role; optionally assigns permission IDs (`:51`)

**Example request:**

```json theme={null}
POST /admin/rbac/roles
{
  "name": "warehouse_manager",
  "display_name": "Warehouse Manager",
  "description": "Manages inventory and orders",
  "permission_ids": ["perm_inventory_manage", "perm_orders_view"]
}
```

### Single role

`src/api/admin/rbac/roles/[id]/route.ts`

* `GET /admin/rbac/roles/:id`
* `POST /admin/rbac/roles/:id` — updates role metadata; if `permission_ids` is provided, it **replaces** the role's permissions (`:42`)
* `DELETE /admin/rbac/roles/:id`

### Role permissions only

`src/api/admin/rbac/roles/[id]/permissions/route.ts`

* `POST /admin/rbac/roles/:id/permissions` — **replaces all** permissions with provided list (`:36`)
* `DELETE /admin/rbac/roles/:id/permissions` — removes **only** the provided permission IDs (`:54`)

> ⚠️ `POST` = full replace, `DELETE` = partial removal. Don't confuse the two — see edge case #4.

### User roles

`src/api/admin/rbac/users/[id]/roles/route.ts`

* `GET /admin/rbac/users/:id/roles`
* `POST /admin/rbac/users/:id/roles` — **syncs** the user's roles to exactly the provided `role_ids`; removes missing roles and adds new ones (`:41`)
* `DELETE /admin/rbac/users/:id/roles` — removes only the listed roles

**Example — sync a user to exactly two roles:**

```json theme={null}
POST /admin/rbac/users/user_123/roles
{
  "role_ids": ["role_catalog_manage", "role_orders_view"]
}
```

If the user previously had `role_pricing_admin`, it is automatically removed since it's not in the list.

### Current user permissions

`src/api/admin/rbac/me/permissions/route.ts`

* `GET /admin/rbac/me/permissions` — returns both `permissions` and `roles`. Useful for front-end capability checks.

**Example response:**

```json theme={null}
{
  "roles": ["catalog_manage", "orders_view"],
  "permissions": ["catalog.manage", "orders.view"]
}
```

### Shared route utilities

`src/api/admin/rbac/utils.ts`

| Helper               | Location |
| -------------------- | -------- |
| `resolveRbacService` | `:12`    |
| `validateBody`       | `:20`    |
| `handleRbacError`    | `:43`    |
| `sendForbidden`      | `:76`    |
| `sendUnauthorized`   | `:86`    |

***

## Seeding roles and permissions

The base RBAC setup is seeded from `src/scripts/seed-rbac.ts`.

```mermaid theme={null}
flowchart TD
    A[Run seed-rbac.ts] --> B[Seed PERMISSIONS list]
    B --> C[Seed PERMISSION_GROUPS as roles]
    C --> D[Seed LEGACY_ROLES: super_admin, all_viewer]
    D --> E["⚠️ Assign super_admin + all_viewer to ALL existing users"]
```

### Permissions

Defined in `PERMISSIONS` — `src/scripts/seed-rbac.ts:29`

**Resources:** `catalog`, `inventory`, `orders`, `customer.phone`, `customer.email`, `customer.address`, `customer.payment_meta`, `customer.internal_notes`, `promotions`, `pricing`, `analytics`, `access`, `exports`, `panel`

**Actions:** `view`, `manage`, `admin`, `run`

### Permission-group style roles

Defined in `PERMISSION_GROUPS` — `:57`

Examples: `catalog_view`, `catalog_manage`, `inventory_admin`, `orders_manage_limited`, `customer_contact_access`, `access_manage`, `panel_admin`

### Legacy roles

Defined in `LEGACY_ROLES` — `:198`

* `super_admin`
* `all_viewer`

### Wildcard support during seeding

Script supports patterns: `*`, `resource.*`, `*.action`, exact `resource.action`. Pattern matching implemented in `:226`.

### Existing user assignment during seed

> ⚠️ After seeding, the script assigns `super_admin` and `all_viewer` to **all existing users** — `src/scripts/seed-rbac.ts:349`. Because `super_admin` resolves to `*`, this gives every existing seeded user **full access**.

***

## Default role assignment for new users

New users automatically receive the `all_viewer` role through subscriber `src/subscribers/user-created-rbac.ts:8`.

```mermaid theme={null}
sequenceDiagram
    participant Medusa
    participant Subscriber as user-created-rbac subscriber
    participant DB

    Medusa->>Subscriber: user.created event
    Subscriber->>DB: fetch role "all_viewer"
    alt Role exists
        DB-->>Subscriber: role found
        Subscriber->>DB: assign role to new user
    else Role missing
        DB-->>Subscriber: not found
        Subscriber->>Subscriber: log warning, skip assignment
    end
```

* Listens to `user.created` — `:40`
* Fetches role `all_viewer` — `:16`
* Assigns it to the new user — `:25`
* If the role does not exist, the subscriber logs a warning and skips assignment

***

## How to add a new permission

```mermaid theme={null}
flowchart LR
    A[1. Add to PERMISSIONS] --> B[2. Add to a PERMISSION_GROUPS role]
    B --> C[3. Re-run seed script]
    C --> D[4. Apply middleware to route]
    D --> E["5. Expose via /admin/rbac/me/permissions"]
```

1. Add the permission seed in `src/scripts/seed-rbac.ts:29`
2. Add that permission into the appropriate permission-group role in `:57`
3. Re-run the RBAC seed script
4. Apply middleware to the route that should be protected
5. If needed, expose the permission to the admin UI using `/admin/rbac/me/permissions`

### Example — adding `comments.manage`

Add to `PERMISSIONS`:

```tsx theme={null}
{ resource: "comments", action: "manage", description: "Manage comments" }
```

Then add it to one or more seeded roles, and protect routes with:

```tsx theme={null}
requirePermission("comments", "manage")
```

***

## How to protect a new route

1. Import `requirePermission` or `requireAnyPermission` from `src/api/admin/rbac-middleware.ts`
2. Create a middleware instance in `src/api/middlewares.ts`
3. Attach it to the desired admin route definition

**Single permission:**

```tsx theme={null}
const reportsView = requirePermission("reports", "view")
```

**Either of two permissions:**

```tsx theme={null}
const reportsAccess = requireAnyPermission(
  { resource: "reports", action: "view" },
  { resource: "analytics", action: "view" }
)
```

***

## Important behaviors and edge cases

1. **`super_admin` is name-based** — the special full-access behavior triggers only when a role is literally named `super_admin` (`src/modules/rbac/service.ts:171`). It does not depend on assigned permissions. If a different role should behave the same way, code must be changed.
2. **Inactive roles do not count** — even if a user is linked to a role, it is ignored if `is_active === false` (`:140`).
3. **API keys bypass RBAC checks** — `api_key` and `api-key` actors are allowed automatically (`src/api/admin/rbac-middleware.ts:37`). Keep this in mind when testing.
4. **Role permission updates replace the relation set** — `assignPermissionsToRole` updates the role with a full permission list (`src/modules/rbac/service.ts:244`). Treat it as a **replace** operation, not an append.
5. **Duplicate role assignment is tolerated** — duplicate link errors return `false` instead of throwing (`:207`).
6. **Global cache invalidation happens on role-permission changes** — when one role's permissions change, all user permission caches become stale by generation bump (`:264`).
7. **Seed script currently grants broad access to all existing users** — the seed script assigns both `super_admin` and `all_viewer` to every existing user (`src/scripts/seed-rbac.ts:349`). Useful for bootstrap, but risky if the seed is run in an environment where granular access is expected.

### Quick scenario check

| Scenario                                                              | Outcome                                                  | Why                                   |
| --------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------- |
| User linked to `super_admin` role, role has zero explicit permissions | Full access (`*`)                                        | Name-triggered, not permission-based  |
| User linked to a role with `is_active: false`                         | Role ignored entirely                                    | Inactive roles excluded at resolution |
| Request comes from an API key                                         | Always allowed                                           | API keys bypass RBAC middleware       |
| Admin calls `POST .../permissions` with 2 IDs, role previously had 5  | Role ends up with only those 2                           | POST replaces, doesn't append         |
| Admin assigns the same role to a user twice                           | Second call returns `false`, no error thrown             | Duplicate-tolerant by design          |
| One role's permissions edited                                         | Every user's cached permissions become stale immediately | Generation bump, not per-key delete   |

***

## Known gaps and developer notes

* **Permission constants are not centralized** — most route protection uses string literals in `src/api/middlewares.ts:50`. Simple, but route strings and seeded permissions can drift apart. A future improvement would centralize permission definitions in a shared constant map.
* **Some protected resources appear outside the seed list** — `src/api/middlewares.ts` includes permissions like `media.manage` (`:56`) and `comments.manage` (`:57`), but these are not present in the seeded `PERMISSIONS` list (`src/scripts/seed-rbac.ts:29`). If these middleware checks are used, the required permissions/roles must exist through another flow or be added to the seed script.
* **Route attachment should always be verified** — `src/api/middlewares.ts` defines permission middleware instances, but when adding new protections you should verify the middleware is actually attached to the intended route registration in that same file.

***

## Quick reference

**Main service methods**

| Method                    | Location                          |
| ------------------------- | --------------------------------- |
| `getUserRoles`            | `src/modules/rbac/service.ts:110` |
| `getUserPermissions`      | `src/modules/rbac/service.ts:145` |
| `checkPermission`         | `src/modules/rbac/service.ts:184` |
| `assignRoleToUser`        | `src/modules/rbac/service.ts:195` |
| `removeRoleFromUser`      | `src/modules/rbac/service.ts:221` |
| `assignPermissionsToRole` | `src/modules/rbac/service.ts:244` |

**Main middleware helpers**

| Helper                 | Location                               |
| ---------------------- | -------------------------------------- |
| `requirePermission`    | `src/api/admin/rbac-middleware.ts:80`  |
| `requireAnyPermission` | `src/api/admin/rbac-middleware.ts:108` |

**Core tables**`role` · `permission` · `role_permission` · `user_role`

**Special roles**

* `super_admin` → full wildcard access
* `all_viewer` → default viewer role for new users

**Useful endpoints**

* `GET /admin/rbac/me/permissions`
* `GET /admin/rbac/roles`
* `POST /admin/rbac/roles`
* `GET /admin/rbac/roles/:id`
* `POST /admin/rbac/roles/:id`
* `DELETE /admin/rbac/roles/:id`
* `POST /admin/rbac/roles/:id/permissions`
* `DELETE /admin/rbac/roles/:id/permissions`
* `GET /admin/rbac/users/:id/roles`
* `POST /admin/rbac/users/:id/roles`
* `DELETE /admin/rbac/users/:id/roles`
