Skip to main content

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.

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

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:

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_rolesrc/links/user-role.ts:16

How permissions work

The permission format is built as resource.actionsrc/modules/rbac/service.ts:56. Supported match patterns Matching logic is implemented in src/modules/rbac/service.ts:93.

Example matches


How user access is resolved

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 Setsrc/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

Read flow

  • 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

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

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

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:

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/permissionsreplaces 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/rolessyncs 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:
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:

Shared route utilities

src/api/admin/rbac/utils.ts

Seeding roles and permissions

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

Permissions

Defined in PERMISSIONSsrc/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 userssrc/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.
  • 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

  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:
Then add it to one or more seeded roles, and protect routes with:

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:
Either of two permissions:

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 checksapi_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 setassignPermissionsToRole 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


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 listsrc/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 verifiedsrc/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 Main middleware helpers Core tablesrole · 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