How to Create an Actor Type

In this document, learn how to create an actor type and authenticate its associated data model.

0. Create Module with Data Model#

Before creating an actor type, you must have a module with a data model representing the actor type.

Tip: Learn how to create a module in this guide.

The rest of this guide uses this Manager data model, defined in a company module, as an example:

src/modules/company/models/manager.ts
1import { model } from "@medusajs/framework/utils"2
3const Manager = model.define("manager", {4  id: model.id().primaryKey(),5  firstName: model.text(),6  lastName: model.text(),7  email: model.text(),8})9
10export default Manager

1. Create Workflow#

Start by creating a workflow that does two things:

  • Creates a record of the Manager data model.
  • Sets the app_metadata property of the associated AuthIdentity record based on the new actor type.

For example, create the file src/workflows/create-manager.ts. with the following content:

src/workflows/create-manager.ts
1import { 2  createWorkflow, 3  createStep,4  StepResponse,5  WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { 8  setAuthAppMetadataStep,9} from "@medusajs/medusa/core-flows"10import CompanyModuleService from "../modules/company/service"11
12type CreateManagerWorkflowInput = {13  manager: {14    first_name: string15    last_name: string16    email: string17  }18  authIdentityId: string19}20
21const createManagerStep = createStep(22  "create-manager-step",23  async ({ 24    manager: managerData,25  }: Pick<CreateManagerWorkflowInput, "manager">, 26  { container }) => {27    const companyModuleService: CompanyModuleService = 28      container.resolve("company")29
30    const manager = await companyModuleService.createManagers(31      managerData32    )33
34    return new StepResponse(manager)35  }36)37
38const createManagerWorkflow = createWorkflow(39  "create-manager",40  function (input: CreateManagerWorkflowInput) {41    const manager = createManagerStep({42      manager: input.manager,43    })44
45    setAuthAppMetadataStep({46      authIdentityId: input.authIdentityId,47      actorType: "manager",48      value: manager.id,49    })50
51    return new WorkflowResponse(manager)52  }53)54
55export default createManagerWorkflow

This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved.

The workflow has two steps:

  1. Create the manager using the createManagerStep.
  2. Set the app_metadata property of the associated auth identity using the setAuthAppMetadataStep from Medusa's core workflows. You specify the actor type manager in the actorType property of the step’s input.

2. Define the Create API Route#

Next, you’ll use the workflow defined in the previous section in an API route that creates a manager.

So, create the file src/api/manager/route.ts with the following content:

src/api/manager/route.ts
1import type { 2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import { MedusaError } from "@medusajs/framework/utils"6import createManagerWorkflow from "../../workflows/create-manager"7
8type RequestBody = {9  first_name: string10  last_name: string11  email: string12}13
14export async function POST(15  req: AuthenticatedMedusaRequest<RequestBody>, 16  res: MedusaResponse17) {18  // If `actor_id` is present, the request carries 19  // authentication for an existing manager20  if (req.auth_context.actor_id) {21    throw new MedusaError(22      MedusaError.Types.INVALID_DATA,23      "Request already authenticated as a manager."24    )25  }26
27  const { result } = await createManagerWorkflow(req.scope)28    .run({29      input: {30        manager: req.body,31        authIdentityId: req.auth_context.auth_identity_id,32      },33    })34  35    res.status(200).json({ manager: result })36}

Since the manager must be associated with an AuthIdentity record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by:

  1. Obtaining a token using the /auth route.
  2. Passing the token in the bearer header of the request to this route.

In the API route, you create the manager using the workflow from the previous section and return it in the response.


3. Apply the authenticate Middleware#

The last step is to apply the authenticate middleware on the API routes that require a manager’s authentication.

To do that, create the file src/api/middlewares.ts with the following content:

src/api/middlewares.ts
1import { 2  defineMiddlewares,3  authenticate,4} from "@medusajs/framework/http"5
6export default defineMiddlewares({7  routes: [8    {9      matcher: "/manager",10      method: "POST",11      middlewares: [12        authenticate("manager", ["session", "bearer"], {13          allowUnregistered: true,14        }),15      ],16    },17    {18      matcher: "/manager/me*",19      middlewares: [20        authenticate("manager", ["session", "bearer"]),21      ],22    },23  ],24})

This applies middlewares on two route patterns:

  1. The authenticate middleware is applied on the /manager API route for POST requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered.
  2. The authenticate middleware is applied on all routes starting with /manager/me, restricting these routes to authenticated managers only.

Retrieve Manager API Route#

For example, create the file src/api/manager/me/route.ts with the following content:

src/api/manager/me/route.ts
1import { 2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/framework/http"5import CompanyModuleService from "../../../modules/company/service"6import {7  ContainerRegistrationKeys,8} from "@medusajs/framework/utils"9
10export async function GET(11  req: AuthenticatedMedusaRequest,12  res: MedusaResponse13): Promise<void> {14  const query = req.scope.resolve(15    ContainerRegistrationKeys.QUERY16  )17  const managerId = req.auth_context?.actor_id18
19  const { data: [manager] } = await query.graph({20    entity: "manager",21    fields: ["*"],22    filters: {23      id: managerId,24    },25  }, {26    throwIfKeyNotFound: true,27  })28
29  res.json({ manager })30}

This route is only accessible by authenticated managers. You access the manager’s ID using req.auth_context.actor_id.


Test Custom Actor Type Authentication Flow#

To authenticate managers:

  1. Send a POST request to /auth/manager/emailpass/register to create an auth identity for the manager:
Code
1curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "manager@gmail.com",5    "password": "supersecret"6}'

Copy the returned token to use it in the next request.

  1. Send a POST request to /manager to create a manager:
Code
1curl -X POST 'http://localhost:9000/manager' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data-raw '{5    "first_name": "John",6    "last_name": "Doe",7    "email": "manager@gmail.com"8}'

Replace {token} with the token returned in the previous step.

  1. Send a POST request to /auth/manager/emailpass again to retrieve an authenticated token for the manager:
Code
1curl -X POST 'http://localhost:9000/auth/manager/emailpass' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "manager@gmail.com",5    "password": "supersecret"6}'
  1. You can now send authenticated requests as a manager. For example, send a GET request to /manager/me to retrieve the authenticated manager’s details:
Code
1curl 'http://localhost:9000/manager/me' \2-H 'Authorization: Bearer {token}'

Whenever you want to log in as a manager, use the /auth/manager/emailpass API route, as explained in step 3.


Delete User of Actor Type#

When you delete a user of the actor type, you must update its auth identity to remove the association to the user.

If the auth identity is shared with other actor types (for example, the same user is both a manager and a customer), you should only remove the manager association. If the manager is the only actor type associated with the auth identity, you should delete the auth identity entirely using deleteAuthIdentityStep.

For example, create the following workflow that deletes a manager and updates its auth identity, create the file src/workflows/delete-manager.ts with the following content:

src/workflows/delete-manager.ts
5import CompanyModuleService from "../modules/company/service"6
7export type DeleteManagerWorkflow = {8  id: string9}10
11const deleteManagerStep = createStep(12  "delete-manager-step",13  async (14    { id }: DeleteManagerWorkflow, 15    { container }) => {16      const companyModuleService: CompanyModuleService = 17        container.resolve("company")18
19      const manager = await companyModuleService.retrieveManager(id)20
21      await companyModuleService.deleteManagers(id)22
23      return new StepResponse(undefined, { manager })24    },25    async ({ manager }, { container }) => {26      const companyModuleService: CompanyModuleService = 27        container.resolve("company")28
29      await companyModuleService.createManagers(manager)30    }31  )

You add a step that deletes the manager using the deleteManagers method of the module's main service. In the compensation function, you create the manager again.

Next, in the same file, add the workflow that deletes a manager:

src/workflows/delete-manager.ts
14} from "@medusajs/medusa/core-flows"15
16// ...17
18export const deleteManagerWorkflow = createWorkflow(19  "delete-manager",20  (21    input: WorkflowData<DeleteManagerWorkflow>22  ): WorkflowResponse<string> => {23    deleteManagerStep(input)24
25    const { data: authIdentities } = useQueryGraphStep({26      entity: "auth_identity",27      fields: ["id", "app_metadata"],28      filters: {29        app_metadata: {30          // the ID is of the format `{actor_type}_id`.31          manager_id: input.id,32        },33      },34    })35
36    const authIdentity = transform(37      { authIdentities },38      ({ authIdentities }) => {39        return authIdentities[0]40      }41    )42
43    const shouldKeepAuthIdentity = transform(44      { authIdentity },45      ({ authIdentity }) => {46        if (!authIdentity) {47          return undefined48        }49        return Object.entries(authIdentity.app_metadata ?? {})50          .filter(([key]) => key !== "manager_id")51          .some(([, value]) => value !== null)52      }53    )54
55    when({ shouldKeepAuthIdentity }, ({ shouldKeepAuthIdentity }) => {56      return shouldKeepAuthIdentity === true57    }).then(() => {58      setAuthAppMetadataStep({59        authIdentityId: authIdentity.id,60        actorType: "manager",61        value: null,62      })63    })64
65    when({ shouldKeepAuthIdentity }, ({ shouldKeepAuthIdentity }) => {66      return shouldKeepAuthIdentity === false67    }).then(() => {68      deleteAuthIdentityStep({69        id: authIdentity.id,70      })71    })72
73    return new WorkflowResponse(input.id)74  }75)

In the workflow, you:

  1. Use the deleteManagerStep defined earlier to delete the manager.
  2. Retrieve the auth identity of the manager using Query. To do that, you filter the app_metadata property of an auth identity, which holds the user's ID under {actor_type_name}_id. So, in this case, it's manager_id.
  3. Check whether the auth identity is shared with other actor types by inspecting the remaining non-null entries in app_metadata.
  4. If the auth identity is shared, call setAuthAppMetadataStep to remove only the manager association. If it is the sole actor type, call deleteAuthIdentityStep to delete the auth identity entirely.

You can use this workflow when deleting a manager, such as in an API route.

Note: The deleteAuthIdentityStep is available since Medusa v2.19.0
Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Medusa MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break