Account switching: preserve the actor and change the buying context
Separate the authenticated person from the account they are shopping for, and authorize each switch using current server-owned relationships.
In this guide
One person, several buying relationships
A purchasing manager signs in at a parent organization and selects a regional account. The storefront now needs that account's catalog, pricing, addresses, and permissions. It still needs to know which person signed in. Replacing the session user indiscriminately can erase the identity that should explain every subsequent action.
Represent the actor and the selected buying context separately. The actor answers who authenticated. The buying context answers which authorized organization and shopper relationship apply to this operation. Both matter when a support team investigates an unexpected price, address, or order.
Selection is a new authorization decision
An account ID submitted by the browser is a request, not proof of access. Load the current actor from trusted authentication state and resolve the target relationship through authoritative server-side data. Check that the account and any selected shopper are active and valid for the relevant store.
Avoid role-name shortcuts such as granting access because a display string contains 'manager'. Define the actual capability and relationship rules. A hierarchy traversal needs cycle detection, depth and result limits, and a clear response when its source is unavailable. Do not turn a failed permission read into an unrestricted account list.
Authorize the requested buying context
Use authorizeBuyingContext in the server-side account selection path. Its Actor comes from trusted authentication, while CurrentGrantReader supplies the current relationship for that actor, account, and store. The function verifies every returned identity rather than treating any non-null grant as sufficient.
Map native account relationships and any explicitly owned supplemental grant policy into CurrentGrantReader. Znode's account permission configuration establishes purchasing permissions; this helper keeps the actor stable while validating a selected context. Repeat current authorization when protected operations execute.
accounts/authorize-buying-context.ts typescript
export type Actor = Readonly<{ userId: number; storeId: number; active: boolean }>;
export type AccountGrant = Readonly<{
actorId: number; accountId: number; storeId: number;
accountActive: boolean; canPurchase: boolean;
}>;
export type BuyingContext = Readonly<{
actorId: number; accountId: number; storeId: number;
}>;
export interface CurrentGrantReader {
find(actorId: number, accountId: number, storeId: number):
Promise<AccountGrant | null>;
}
export async function authorizeBuyingContext(
authenticatedActor: Actor, requestedAccountId: number,
grants: CurrentGrantReader
): Promise<BuyingContext> {
if (!authenticatedActor.active ||
!Number.isSafeInteger(authenticatedActor.userId) || authenticatedActor.userId < 1 ||
!Number.isSafeInteger(authenticatedActor.storeId) || authenticatedActor.storeId < 1 ||
!Number.isSafeInteger(requestedAccountId) || requestedAccountId < 1)
throw new Error("A valid active context is required.");
const grant = await grants.find(authenticatedActor.userId,
requestedAccountId, authenticatedActor.storeId);
if (!grant || !grant.accountActive || !grant.canPurchase ||
grant.actorId !== authenticatedActor.userId ||
grant.accountId !== requestedAccountId ||
grant.storeId !== authenticatedActor.storeId)
throw new Error("Account access denied.");
return Object.freeze({
actorId: authenticatedActor.userId,
accountId: requestedAccountId,
storeId: authenticatedActor.storeId
});
}
// Supply Actor from trusted server authentication, never request JSON.
// CurrentGrantReader maps current platform relationships into this policy.
Invalidate the old account's projections
Changing a label in the header is only the visible part of switching. Prices, cart data, address selections, account documents, and cached API responses may still belong to the old context. Identify each projection and decide whether it is replaced, invalidated, or blocked until a fresh read completes.
Never migrate a cart between accounts just because the product list happens to match. Catalog access, quantity rules, contracts, and shipping restrictions may differ. If the business supports carrying selections across accounts, model that as a new validated operation. Preserve a clear boundary between remembered product intent and an authorized purchasable cart.
Test relationship changes, not just happy switches
Use fictional accounts with visibly different catalogs or prices. Verify a permitted switch, an unauthorized target, an expired context, a removed relationship, and a locked actor. Try accessing a document directly after switching rather than relying on navigation to hide it.
Exercise hierarchy cycles and a permission-service outage. Verify that the actor remains unchanged in audit context while the selected account changes. Then run concurrent requests during a switch and ensure an old account response cannot populate the new account's screen. Retain these relationship-change cases in the account-context regression suite.
- Authorize account-scoped reads and writes independently.
- Keep account and store dimensions in relevant cache keys.
- Do not log raw session tickets or authentication material.
Make the distinction visible to buyers
A compact account indicator should name the active buying organization and provide a deliberate way to switch. If switching invalidates an in-progress cart, explain the transition before losing useful input. Clear context is part of preventing purchasing mistakes, not just a convenience feature.
Document who can select which accounts and why. That policy becomes the shared reference for backend authorization, storefront behavior, and support. When the actor remains stable and buying context is explicit, the system can support complex organizations without making identity depend on whichever account was clicked most recently.
References and further reading
Bring your next engineering question.
Design complex B2B account experiences
Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.