> Documentation index: [Saleor](/llms.txt) · [This section](/developer/llms.txt)
> Source: https://docs.saleor.io/developer/users/user-model

# Customers and Staff as One User

Customers and staff are not separate entities in Saleor. Both are represented by the same [`User`](/api-reference/users/objects/user.md) GraphQL type. The role of a given user is determined by the `isStaff` field and by which mutations and permissions apply to them.

This page explains how the two roles relate at the API level: which fields are meaningful for each, which mutations to use to create or manage them, and how a user can move between roles.

<a id="one-type-two-roles"></a>

## One type, two roles

Every account — whether a shopper or an administrator — is a `User`. The same fields (`id`, `email`, `firstName`, `lastName`, `addresses`, `metadata`, `orders`, etc.) are available on both. The difference is:

-   **`isStaff: Boolean!`** — `true` for staff members, `false` for customers. It is exposed publicly on the `User` type and is also included as a claim in the JWT issued by [`tokenCreate`](/api-reference/authentication/mutations/token-create.md).
-   **Role-specific fields** — see below.
-   **Permissions required to manage them** — `MANAGE_USERS` for customers, `MANAGE_STAFF` for staff.

<a id="fields-that-are-meaningful-only-for-staff"></a>

### Fields that are meaningful only for staff

The following fields exist on every `User` but are only populated for staff. For customers they return empty lists or `false`:

| Field | Meaning |
| --- | --- |
| `permissionGroups` | Permission groups the user belongs to. Customers cannot belong to permission groups via the public API. |
| `userPermissions` | Effective permissions granted directly or via groups. |
| `editableGroups` | Groups the user is allowed to manage (used by the Dashboard). |
| `accessibleChannels` | Channels the staff user has access to via their permission groups. |
| `restrictedAccessToChannels` | Whether the staff user's access is limited to a subset of channels. |

See [Permissions](/developer/permissions.md) for how permission groups and channel restrictions work.

<a id="choosing-a-create-mutation"></a>

## Choosing a create mutation

There are three mutations that produce a `User`. Pick the one that matches the caller and the resulting role:

| Mutation | Caller | Required permission | Resulting role |
| --- | --- | --- | --- |
| [`accountRegister`](/api-reference/users/mutations/account-register.md) | The user themselves (storefront) | None | Customer |
| [`customerCreate`](/api-reference/users/mutations/customer-create.md) | Staff or app | `MANAGE_USERS` | Customer |
| [`staffCreate`](/api-reference/users/mutations/staff-create.md) | Staff or app | `MANAGE_STAFF` | Staff (`isStaff: true`) |

`accountRegister` and `customerCreate` both produce a customer; the difference is who initiates the action and whether a password is set up-front or via an emailed link. See [Manage Account as a Customer](/developer/users/customer-account.md) and [Customer Management](/developer/users/customer-management.md).

<a id="promoting-a-customer-to-staff"></a>

### Promoting a customer to staff

If `staffCreate` is called with an email that already belongs to a customer, the existing `User` is reused and promoted to staff. The same `id`, addresses, metadata, and order history are preserved — only `isStaff` flips to `true` and any provided groups are assigned.

There is no public mutation to demote a staff user back to a customer. To revoke staff access, use [`staffDelete`](/api-reference/users/mutations/staff-delete.md) or remove the user from all permission groups via [`staffUpdate`](/api-reference/users/mutations/staff-update.md).

<a id="end-to-end-lifecycle-self-registration-then-staff-invite"></a>

#### End-to-end lifecycle: self-registration, then staff invite

A common flow that causes confusion: a shopper signs up through the storefront, then is later invited by an administrator to become staff. Because both roles share the same `User`, the order history, addresses, and metadata gathered as a customer carry over after the promotion.

The table below lists, for each mutation in this flow, the asynchronous events emitted and how the corresponding email is delivered. The split is important:

-   **Staff emails** (invite / set password / password reset for staff) are sent by the **core admin email plugin** when it is configured. No external app is required.
-   **Customer emails** (account confirmation, set password, password reset for customers) are **not** sent by Saleor core. An external app must subscribe to the corresponding webhook and deliver the email itself — typically [Customer Emails](/developer/app-store/apps/customer-emails/overview.md) on Saleor Cloud, or the [SMTP app](/developer/app-store/apps/smtp/overview.md) if you are self-hosting.

| # | Mutation | Async events emitted | Email |
| --- | --- | --- | --- |
| 1 | [`accountRegister`](/api-reference/users/mutations/account-register.md) | `CUSTOMER_CREATED`; additionally `ACCOUNT_CONFIRMATION_REQUESTED` if [`enableAccountConfirmationByEmail`](/api-reference/miscellaneous/objects/shop.md#enable-account-confirmation-by-email) is `true` | Customer confirmation email. **Not sent by core** — requires an external app (e.g. [Customer Emails](/developer/app-store/apps/customer-emails/overview.md) on Cloud, or the [SMTP app](/developer/app-store/apps/smtp/overview.md) when self-hosting) subscribed to `ACCOUNT_CONFIRMATION_REQUESTED`. |
| 2 | [`confirmAccount`](/api-reference/users/mutations/confirm-account.md) | `ACCOUNT_CONFIRMED` | No email required for the confirmation step itself. If you want a "welcome" email, subscribe an external app to `ACCOUNT_CONFIRMED`. |
| 3 | [`accountUpdate`](/api-reference/users/mutations/account-update.md) (customer edits own profile) | `CUSTOMER_UPDATED`; `CUSTOMER_METADATA_UPDATED` if metadata changed | None by default. |
| 4 | [`requestPasswordReset`](/api-reference/users/mutations/request-password-reset.md) (while still a customer) | `ACCOUNT_SET_PASSWORD_REQUESTED` | Customer password reset email. **Not sent by core** — requires an external app subscribed to `ACCOUNT_SET_PASSWORD_REQUESTED`. |
| 5 | [`setPassword`](/api-reference/users/mutations/set-password.md) | No webhook event is emitted by this mutation itself. | No email. |
| 6 | [`staffCreate`](/api-reference/users/mutations/staff-create.md) called with the existing customer's email | `STAFF_CREATED` (the same `User.id` is reused; `isStaff` becomes `true`). Additionally `STAFF_SET_PASSWORD_REQUESTED` if `redirectUrl` is provided. | Staff invite / set password email. **Sent by the core admin email plugin** when configured. The customer-facing `ACCOUNT_SET_PASSWORD_REQUESTED` event is **not** emitted in this case, so an external customer-email app does not receive a duplicate. |
| 7 | [`staffUpdate`](/api-reference/users/mutations/staff-update.md) | `STAFF_UPDATED` | None by default. |
| 8 | [`requestPasswordReset`](/api-reference/users/mutations/request-password-reset.md) (now as a staff user) | `STAFF_SET_PASSWORD_REQUESTED` | Staff password reset email. **Sent by the core admin email plugin** when configured. |
| 9 | [`setPassword`](/api-reference/users/mutations/set-password.md) | No webhook event is emitted by this mutation itself. | No email. |

Customer emails require an external app

Saleor core ships an "admin email" plugin that covers **staff** notifications (staff invite, staff password reset, etc.) out of the box. It does **not** send emails to customers. To deliver account confirmation, customer password reset, order confirmation, and other customer-facing emails, install [Customer Emails](/developer/app-store/apps/customer-emails/overview.md) on Saleor Cloud, or the [SMTP app](/developer/app-store/apps/smtp/overview.md) if you are self-hosting, and subscribe it to the relevant `ACCOUNT_*` and `CUSTOMER_*` webhook events.

Which event fires depends on `isStaff` at the time of the request

`requestPasswordReset` branches on the user's current role: it emits `ACCOUNT_SET_PASSWORD_REQUESTED` for customers and `STAFF_SET_PASSWORD_REQUESTED` for staff. Once a user has been promoted via `staffCreate`, all subsequent password-related notifications go through the staff event and the admin email plugin.

<a id="choosing-an-update-or-delete-mutation"></a>

## Choosing an update or delete mutation

Update and delete mutations are split along the same lines as creation:

| Action | Self-service | Customer (by staff) | Staff (by staff) |
| --- | --- | --- | --- |
| Update | [`accountUpdate`](/api-reference/users/mutations/account-update.md) | [`customerUpdate`](/api-reference/users/mutations/customer-update.md) | [`staffUpdate`](/api-reference/users/mutations/staff-update.md) |
| Delete | [`accountDelete`](/api-reference/users/mutations/account-delete.md), [`accountRequestDeletion`](/api-reference/users/mutations/account-request-deletion.md) | [`customerDelete`](/api-reference/users/mutations/customer-delete.md) | [`staffDelete`](/api-reference/users/mutations/staff-delete.md) |

The `account*` mutations always operate on the currently authenticated user and never change the user's role. The `customer*` and `staff*` mutations require the matching permission and only operate on users of the corresponding role.

<a id="querying-customers-and-staff"></a>

## Querying customers and staff

The API exposes role-scoped list queries and a single by-identity lookup:

-   [`customers`](/api-reference/users/queries/customers.md) — requires `MANAGE_USERS` or `MANAGE_ORDERS`. Returns customer accounts. **Staff users who have placed an order also appear in this list**, because order history is tied to the underlying `User`.
-   [`staffUsers`](/api-reference/users/queries/staff-users.md) — requires `MANAGE_STAFF`. Returns staff accounts only.
-   [`user`](/api-reference/users/queries/user.md) — lookup by `id`, `email`, or `externalReference`. The result is scoped by the caller's permissions: `MANAGE_STAFF` resolves staff, `MANAGE_USERS` or `MANAGE_ORDERS` resolves customers, holding both resolves any user.
-   [`me`](/api-reference/users/queries/me.md) — returns the currently authenticated user regardless of role.

<a id="dual-role-behavior"></a>

### Dual-role behavior

Because a staff member and a customer are the same kind of entity, a single `User` can act in both roles at once. A staff user who places an order keeps their `isStaff: true` but will also be returned by the `customers` query. Their order history, addresses, and metadata are all on the same record.

When building integrations, treat the role as a property of the user (`isStaff`) rather than as a separate identity. Do not assume that "appears in `customers`" implies "is not staff".

<a id="authentication"></a>

## Authentication

Customers and staff authenticate against the same endpoints and receive tokens of the same shape:

-   [`tokenCreate`](/api-reference/authentication/mutations/token-create.md) issues an access token and refresh token for either role. The decoded token includes the `is_staff` claim, but permissions are not embedded — they are resolved on each request from the user's groups and direct permissions.
-   [`tokenRefresh`](/api-reference/authentication/mutations/token-refresh.md), [`tokenVerify`](/api-reference/authentication/mutations/token-verify.md), and [`tokensDeactivateAll`](/api-reference/authentication/mutations/tokens-deactivate-all.md) work the same for both roles.

The [`passwordLoginMode`](/api-usage/authentication.md#password-login-mode) shop setting can restrict password login for staff while leaving it available for customers. See [Authentication](/api-usage/authentication.md) for the full token lifecycle and JWT structure.

<a id="permission-groups-are-staff-only"></a>

## Permission groups are staff-only

Permission groups (`permissionGroupCreate`, `permissionGroupUpdate`, `permissionGroupDelete`) require `MANAGE_STAFF` and only operate on staff members. Customers cannot be added to a permission group through the public API, and the `permissionGroups` field on a customer's `User` will always be empty.

If you need to grant a customer access to staff capabilities, promote them to staff via [`staffCreate`](/api-reference/users/mutations/staff-create.md) first.
