In v3 a Role carried both its permissions and the Channels it applied to, and a User held Roles directly. In v4 a Role is a set of permissions owned by one Channel. A RoleAssignment grants a Role to a User on one Channel:
The same Role can now be granted to different Users on different Channels, so a multi-vendor marketplace no longer needs a copy of each Role per seller.
Where a Role may be assigned depends on the Channel that owns it:
A Role is owned by the Channel it was created on, and keeps that owner. Deleting a Channel deletes the Roles it owns. On a single-channel store every Role is a shared Role, so nothing changes.
What else changes with it:
User.roles and Role.channels are removed. A new role_assignment table replaces the user_roles_role and role_channels_channel join tables.Authenticated permission on each Channel they are a member of, and nowhere else.CreateRole, ReadRole, UpdateRole and DeleteRole permissions, bundled in a new RoleEditor system Role, on the Channel that owns the Role.Run previewRoleAssignmentMigration after updating the Vendure packages and before migrating, while the database still has its v3 tables. It only reads, so it is safe to run against production:
With synchronize: true, starting Vendure v4 migrates the data straight away. Run the preview before you start v4 for the first time.
The report has these sections:
UpdateRole on the default Channel can edit them. To keep a Role private to each Channel, create one Role per Channel after migrating.Done when: you have acted on the sections marked "action needed", and noted the "Users with role assignments" and "Role assignments to be written" counts to compare after migrating.
In v3 a User received each of their Roles' permissions on every Channel the Role was assigned to. migrateRoleAssignmentData writes out that cross product as explicit (user, role, channel) rows, with these exceptions:
Authenticated on the Channels they are a member of (the customer_channels_channel table), derived at check time.It gives every Role its owning Channel. A Role assigned to exactly one Channel is owned by that Channel. Every other Role, assigned to several Channels, to none or only to the default Channel, is owned by the default Channel. All existing grants stay valid under these owners.
It also creates the RoleEditor Role, without granting it to anyone, and adds the new Role permissions to the SuperAdmin Role. It leaves the v3 tables user_roles_role and role_channels_channel in place, so v3 servers keep working until the last one is replaced. removeLegacyRoleData then drops them and deletes the Customer role. The backup taken before the migration is the way back to v3, and holds the v3 grants should you need them later.
Call both helpers in your v4 migration after the generated statements that create the role_assignment table. For a rolling deployment, call removeLegacyRoleData from a later migration instead. See Database migration for the whole migration.
When it succeeds, migrateRoleAssignmentData logs a line like this:
Done when: the role assignment and user counts in that line match the preview's "Role assignments to be written" and "Users with role assignments".
With dbConnectionOptions.synchronize enabled, there is no migration to call them from. Vendure runs both on the first v4 startup instead, in one transaction. Run the preview first all the same.
Table names use the entityPrefix of your connection options.
Vendure v4 refuses to start while the user_roles_role table holds rows and migrateRoleAssignmentData has not run. Without this check it would start with an empty role_assignment table and lock out every Administrator except the configured superadmin.
After copying, migrateRoleAssignmentData checks that role_assignment holds exactly the grants the v3 tables describe under the rules above. If any row is missing or extra, it throws an error that names the affected Users, Roles and Channels, and does not record the copy, so v4 still refuses to start. It records the copy in a role_assignment_migration table, which removeLegacyRoleData reads and then drops.
Between migrateRoleAssignmentData and removeLegacyRoleData, v3 and v4 servers can serve the same shop. v4 works fully, but the two do not see each other's role changes:
role_assignment when removeLegacyRoleData runs. Other changes made on a v3 server are not carried over. removeLegacyRoleData lists them as "Not applied".removeLegacyRoleData deletes it.Do not change Roles, Administrators, API keys or Channels while both run. v4 logs a warning on startup until removeLegacyRoleData has run.
Retiring the v3 servers is also a security step. Every v3 server still applies the v3 rules, for instance Role changes under UpdateAdministrator, against the role table which v4 reads too. A permission added to a shared Role on a v3 server reaches its v4 holders on every Channel where v4 assigned it, which the v3 check never sees. Keep the rolling window short, and do not give Administrators access to a v3 server once v4 serves traffic.
A Customer who registers on a v4 server while v3 servers still run gets no v3 Customer role, so v3 servers refuse their customer-scoped requests, such as activeCustomer, with a ForbiddenError. Requests which reach a v4 server work. Once the last v3 server is replaced, everything works again without the Customer logging in anew. Existing Customers and guest checkouts are not affected. To avoid it, replace the v3 servers at a quiet time, or route each session to one server (sticky sessions).
If v3 and v4 servers share a session cache, for example DefaultSessionCacheStrategy over Redis or the database, v3 servers also refuse most requests of a SuperAdmin with a ForbiddenError, for up to sessionCacheTTL each time a v4 server caches their session. v4 caches SuperAdmin access in globalPermissions, which v3 does not read. Requests which reach a v4 server work, and other Administrators are not affected. To avoid it, replace the v3 servers at a quiet time, or use sticky sessions.
roleAssignments query in the Admin API as a SuperAdmin. It returns the migrated rows.Done when: both checks pass.
A cached session keeps the permissions it was cached with until it expires after sessionCacheTTL seconds (300 by default). A session cached by v3 grants Customers Authenticated on every Channel, and v4 uses it as it is. Without further action, v4 uses sessions cached by v3 for up to sessionCacheTTL after the last v3 server stops.
To end that window at once, clear the session cache after the last v3 server has stopped, and before the first v4 server takes traffic if your deployment allows it. Clearing it earlier does not help, because v3 servers keep caching sessions with v3 permissions. If the window is acceptable, wait sessionCacheTTL instead.
Sessions themselves stay in the database, so nobody is logged out. Each session is loaded from the database again on its next request.
CacheStrategy (in memory): nothing to do. Each server has its own cache, and a v4 server starts with an empty one.RedisCachePlugin: delete the keys matching vendure-cache:vendure-session-cache:*, for example with redis-cli --scan --pattern 'vendure-cache:vendure-session-cache:*' | xargs redis-cli del.DefaultCachePlugin: delete the rows of the cache_item table whose key starts with vendure-session-cache:. Their cache_tag rows are deleted with them.SessionCacheStrategy: delete the sessions from its store.The names above use the default namespace of the RedisCachePlugin, the default cachePrefix of the DefaultSessionCacheStrategy, and no entityPrefix. Adjust them if you changed any of these.
Done when: the session cache holds no vendure-session-cache: entries, or sessionCacheTTL has passed since the last v3 server stopped.
In v3 the Administrator permissions covered Role CRUD. In v4 Role CRUD needs CreateRole, UpdateRole and DeleteRole, bundled in the RoleEditor Role, which the migration grants to nobody. Until a SuperAdmin grants it, only SuperAdmins can create, edit or delete Roles.
An Administrator can edit or delete a Role only when they hold UpdateRole or DeleteRole on the Channel that owns it. To add permissions to a Role, they must also hold those permissions on the owning Channel and on every Channel where the Role is assigned. Grant RoleEditor on the default Channel to the administrators who maintain the shared Roles, and on a seller's Channel to the administrators who maintain that seller's own Roles.
A v3 Customer could hold a Role besides the Customer role, for example to unlock features of a B2B plugin. The migration copies those grants. They keep working for plugin operations which check them on the Shop API. They do not reach the Admin API: since GHSA-5fpx-9m35-w745 (v3.8.x) the Admin API refuses a session created on the Shop API.
assignRolesToUser and removeRolesFromUser accept only Administrators and API keys, so these grants cannot be changed through the Admin API. Change them in code with RoleAssignmentService.createAssignments() and deleteAssignments(), which perform no authorization and so belong in trusted plugin code only.
The role-assignments codemod makes the mechanical edits:
Without a path it runs in the current directory. It edits:
.graphql files and gql / graphql templates): removes channels from Role selections, and channelIds from inline createRole / updateRole inputs.relations options: removes User.roles and Role.channels paths.Code whose replacement depends on what it is meant to do, such as reading user.roles, is left for you. The codemod marks it with a comment naming the replacement and logs each one:
Search for TODO(vendure-v4) and work through each comment using the sections below. Running the codemod again adds no duplicate comments. It does not rewrite RoleChangeEvent subscribers (see Events). For the full list of code it does not change, see Finding code to update.
You do not need to change:
@Allow() decorators and the permissions they name.PermissionDefinition. SuperAdmins hold them automatically, as before.RequestContext.userHasPermissions() and userHasAllPermissions() without a channelId. They check the active Channel, as before.User.roles and Role.channels are removed. Reading either is a compile error, and loading either through relations makes TypeORM throw.
RoleAssignmentService.resolveUserRoles(ctx, userId).RoleAssignment entity, for example { where: { roleId } }.user_roles_role or role_channels_channel must join role_assignment instead.Role is no longer ChannelAware. ChannelService.assignToChannels() and removeFromChannels() no longer accept a Role.Role has a new channel / channelId: the owning Channel. RoleService.create() sets it to the active Channel. A Role saved directly through a repository without an owner cannot be granted until the next server start, which makes it a shared Role.RoleAssignment entity has userId, roleId and channelId.To grant or revoke a Role on a Channel, use RoleAssignmentService.assign() and remove(). Both check that the active user may grant each Role on its Channel.
| v3 | v4 |
|---|---|
RoleService.getChannelsForRole() | Query the RoleAssignment entity by roleId. |
RoleService.assignRoleToChannel() | RoleAssignmentService.assign() |
RoleService.getCustomerRole() | Removed. There is no Customer role. |
RoleService.assertActiveUserCanGrantRoles(ctx, roleIds) | Takes (roleId, channelId) pairs. |
UserService.createApiKeyUser(ctx, identifier, roles) | createApiKeyUser(ctx, identifier), then RoleAssignmentService.assign(). |
ExternalAuthenticationService.createAdministratorAndUser() | Pass roleAssignments: [{ role, channelId }] to name the Channel of each grant. The deprecated roles grants a Role owned by a non-default Channel on that Channel, and a shared Role on the active Channel, which the client chooses with the vendure-token header. A Role is never granted on a Channel where it may not be assigned. In v3 the Roles applied on their own Channels. |
RoleChangeEvent is removed. It was keyed on the Administrator and carried only Role ids, so it could not express the Channel of a grant. Subscribe to RoleAssignmentEvent instead:
RoleAssignmentEvent has user, assignments (the { roleId, channelId } pairs the write changed) and type ('assigned' or 'removed'). It fires in more cases than RoleChangeEvent did:
event.user.id.assignRolesToUser, removeRolesFromUser, Administrator and API key creation and updates, and SSO Administrator creation. RoleChangeEvent fired only on updateAdministrator with roleIds.removed for the rows it removes.AdministratorEvent still fires as before, but event.entity.user.roles is gone with the relation.
RoleService.getCustomerRole() is removed. A Customer holds Authenticated on each Channel they are a member of, exported as CUSTOMER_PERMISSIONS from @vendure/core.CUSTOMER_ROLE_CODE and CUSTOMER_ROLE_DESCRIPTION in @vendure/common are deprecated.CachedSessionUser has a new globalPermissions property. It holds the permissions that apply on every Channel, which is how SuperAdmin access is stored. A custom SessionCacheStrategy that stores selected fields must include it.RequestContext.userHasPermissions() and userHasAllPermissions() take an optional channelId. The default is the active Channel.Permission.SuperAdmin instead of reading the Role's permission list.Removed:
Role.channelschannelIds on CreateRoleInput and UpdateRoleInputcreateRole creates the Role on the active Channel, the one in the vendure-token header. Send it with the default Channel's token to create a shared Role.
Added:
assignRolesToUser adds pairs, and removeRolesFromUser removes them. Both skip pairs that need no change.UpdateAdministrator (or UpdateApiKey for an API key's User) on the Channel of every pair, not only on the active Channel. The roleAssignments input of createAdministrator and createApiKey needs CreateAdministrator or CreateApiKey on the Channel of every pair.roleAssignments and User.roleAssignments return the assignments of the Users the active user has authority over, all of them, and nothing for other Users. So the list shows exactly the assignments the active user may change.createAdministrator and createApiKey accept a roleAssignments input.Role.channel returns the owning Channel.ReadAdministrator on every Channel can see them, and UpdateAdministrator (or DeleteAdministrator) on every Channel to change them, assign them Roles or delete them. Otherwise an Administrator of one Channel could take over a suspended Administrator of another.createRole and updateRole refuse the codes of the system Roles (__super_admin_role__, __role_editor_role__) and of the v3 Customer role.The roleIds input on createAdministrator, updateAdministrator, createApiKey and updateApiKey, and the assignRoleToAdministrator mutation, still work but are deprecated.
They now apply to the active Channel only, the one in the vendure-token header. In v3 roleIds replaced an Administrator's Roles everywhere, and the Channels came from each Role. Single-channel stores see no difference. Multi-channel clients should move to assignRolesToUser and removeRolesFromUser, which take the id of the Administrator's User (administrator.user.id):
Other changes to these inputs:
CreateAdministratorInput.roleIds is now optional.roleIds and roleAssignments to a create mutation is an error.roleIds fails as a whole if it would revoke a pair the active user may not revoke.If you are upgrading from 3.7.3 or later, these already apply:
apiKeys, apiKey, updateApiKey, rotateApiKey and deleteApiKeys only see or change a key whose User holds role assignments the actor may grant. ApiKeyService.create() refuses to bind a key to an existing User outside the actor's authority.updateChannel and deleteChannel check UpdateChannel / DeleteChannel on the target Channel, not the active one.e2e/__data__ after upgrading. The cached database still has the v3 tables, and v4 refuses to start on it instead of migrating.SimpleGraphQLClient.asUserWithCredentials() switches the client to a Channel token when the login returns exactly one Channel. Customers now get only the Channels they are a member of, so this happens more often. Set the token you want again after logging in.Single-channel storefronts are not affected.
In v3 the Customer role was assigned to every Channel, so a Customer of Channel A could use customer-scoped operations, such as activeCustomer, on Channel B. In v4 a Customer holds Authenticated only on the Channels they are a member of.
With the default CustomerChannelAssignmentStrategy, nothing changes for the shopper: a signed-in Customer becomes a member of a Channel on their first request there, and that first request already succeeds.
With a CustomerChannelAssignmentStrategy that declines the assignment, customer-scoped operations on that Channel now fail:
ForbiddenError (GraphQL error code FORBIDDEN), not an ErrorResult.Authenticated there.In v3, a declining strategy only meant the membership was not saved, and the request went through. If Customers should keep access to such a Channel, add the memberships before migrating. The preview lists, per Channel, how many Customers lose access.
The channels returned by login, authenticate and me used to list every Channel of the instance for a Customer. In v4 it lists the Channels the Customer is a member of, and grows as they visit new Channels. Storefront code that builds a Channel picker from this list, or treats a single entry as a special case, sees fewer entries.
Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team