Adding Custom Actors

May 1, 2026 · 5 min read

Actors in medusa-permissions represent entities that perform actions in your system. While the plugin comes with built-in actors for users and customers, you can create custom actors to support additional entity types like vendors, partners, or team members.

Understanding Actor Providers

Actor providers are responsible for:

  1. Resolving actors to their associated roles
  2. Listing roles for a specific actor
  3. Managing actor-role assignments
  4. Listing actors of a specific type

Actor Provider Structure

All custom actor providers extend AbstractActorResolver:

export abstract class AbstractActorResolver {
  static identifier: string;
  static display_name: string;
  static actor_type: string;

  constructor(
    public readonly actor_type: string,
    protected readonly container: MedusaContainer["cradle"],
    protected readonly options: Record<string, any>
  ) {}

  abstract getActorDetails(input: ActorResolverInput): Promise<ActorResolverDetailsOutput>;
  abstract listRoles(input: ActorResolverInput): Promise<ActorResolverOutput>;
  abstract listActors(input: ActorResolverListInput): Promise<ActorResolverListOutput[]>;
  abstract updateActorRoles(input: ActorResolverUpdateRolesInput): Promise<ActorResolverOutput>;
}

Creating a Custom Actor: Vendor Example

Here's a complete example of creating a vendor actor provider:

import { MedusaContainer } from "@medusajs/framework/types"
import { 
  AbstractActorResolver,
  type ActorResolverInput,
  type ActorResolverDetailsOutput,
  type ActorResolverOutput,
  type ActorResolverListInput,
  type ActorResolverListOutput,
  type ActorResolverUpdateRolesInput
} from "medusa-permissions"

class VendorActorResolver extends AbstractActorResolver {
  static identifier = "vendor"
  static display_name = "Vendor"
  static actor_type = "vendor"

  constructor(
    public readonly actor_type: string,
    protected readonly container: MedusaContainer["cradle"],
    protected readonly options: Record<string, any>
  ) {
    super(actor_type, container, options)
  }

  async getActorDetails(input: ActorResolverInput): Promise<ActorResolverDetailsOutput> {
    const { actor_id } = input;

    // Query your vendor entity
    const vendorService = this.container.vendorService;
    const vendor = await vendorService.retrieveVendor(actor_id);

    if (!vendor) {
      throw new Error(`Vendor with ID ${actor_id} not found`);
    }

    return {
      actor_id: vendor.id,
      actor_type: this.actor_type,
      display_name: vendor.name,
      metadata: {
        email: vendor.email,
        status: vendor.status
      }
    };
  }

  async listRoles(input: ActorResolverInput): Promise<ActorResolverOutput> {
    const { actor_id } = input;
    const permissionsService = this.container.permissionsService;

    // Get roles assigned to this vendor
    const roles = await permissionsService.listActorRoles({
      actor_id,
      actor_type: this.actor_type
    });

    return {
      actor_id,
      actor_type: this.actor_type,
      roles: roles.map(r => ({
        id: r.id,
        name: r.name,
        priority: r.priority
      }))
    };
  }

  async listActors(input: ActorResolverListInput): Promise<ActorResolverListOutput[]> {
    const { limit = 50, offset = 0 } = input;

    // Query your vendor service
    const vendorService = this.container.vendorService;
    const vendors = await vendorService.listVendors({
      take: limit,
      skip: offset
    });

    return vendors.map(vendor => ({
      actor_id: vendor.id,
      actor_type: this.actor_type,
      display_name: vendor.name,
      metadata: {
        email: vendor.email
      }
    }));
  }

  async updateActorRoles(input: ActorResolverUpdateRolesInput): Promise<ActorResolverOutput> {
    const { actor_id, role_ids } = input;
    const permissionsService = this.container.permissionsService;

    // Update vendor roles
    await permissionsService.assignRolesToActor({
      actor_id,
      actor_type: this.actor_type,
      role_ids
    });

    return this.listRoles({ actor_id });
  }
}

export default VendorActorResolver;

Type Definitions

ActorResolverInput

interface ActorResolverInput {
  actor_id: string;
}

ActorResolverDetailsOutput

interface ActorResolverDetailsOutput {
  actor_id: string;
  actor_type: string;
  display_name: string;
  metadata?: Record<string, any>;
}

ActorResolverOutput

interface ActorResolverOutput {
  actor_id: string;
  actor_type: string;
  roles: Array<{
    id: string;
    name: string;
    priority?: number;
  }>;
}

ActorResolverListInput

interface ActorResolverListInput {
  limit?: number;
  offset?: number;
}

ActorResolverListOutput

interface ActorResolverListOutput {
  actor_id: string;
  actor_type: string;
  display_name: string;
  metadata?: Record<string, any>;
}

ActorResolverUpdateRolesInput

interface ActorResolverUpdateRolesInput {
  actor_id: string;
  role_ids: string[];
}

Registering Your Actor

Add your custom actor to medusa-config.ts:

{
  resolve: "medusa-permissions/modules/permissions",
  options: {
    permissions: [
      {
        resolve: "medusa-permissions/permissions/admin"
      }
    ],
    actors: [
      {
        resolve: "medusa-permissions/actors/user"
      },
      {
        resolve: "medusa-permissions/actors/customer"
      },
      {
        resolve: "./src/services/permissions/vendor-actor" // Your custom actor
      }
    ]
  }
}

Integration with Middlewares

Once your actor is registered, it can be used in permission middlewares:

import { validatePermission } from "medusa-permissions/utils/permission-middleware";

export default defineMiddlewares({
  routes: [
    {
      method: "GET",
      matcher: "/admin/vendor/:id",
      middlewares: [
        validatePermission("vendor.orders.list")
      ]
    }
  ]
});