Skip to content

Sync cart buyer identity when auth status changes - #3913

Merged
fredericoo merged 8 commits into
previewfrom
fb-cart-customer-identity-sync
Aug 18, 2026
Merged

fredericoo merged 8 commits into
previewfrom
fb-cart-customer-identity-sync

Conversation

@fredericoo

@fredericoo fredericoo commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: Cart handlers can now associate newly created carts with the current Customer Account session, while Customer Account routes expose lifecycle hooks for application-owned synchronization of existing carts.

Before

Cart creation did not include Customer Account buyer identity, and the standard authorization, refresh, and logout routes offered no integration points for related session work.

After

const cartHandlers = createCartServerHandlers({customerSession});

const updateCartBuyerIdentity = async (context, customerAccessToken) => {
  const cartId = await getVerifiedCartId(context.sessionManager);
  if (!cartId) return;

  await context.storefrontClient.graphql(CART_BUYER_IDENTITY_UPDATE, {
    variables: {
      cartId,
      buyerIdentity: {customerAccessToken},
    },
  });
};

const attachCartBuyerIdentity = async (context, accessToken) => {
  await updateCartBuyerIdentity(context, accessToken);
};

const refreshCartBuyerIdentity = async (context, result) => {
  if (result.status === "transient") return;
  await updateCartBuyerIdentity(context, result.accessToken ?? null);
};

const customerAccountHandlers = createCustomerAccountServerHandlers({
  customerSession,
  onAuthenticated: attachCartBuyerIdentity,
  onTokenRefresh: refreshCartBuyerIdentity,
  onLogout: (context) => updateCartBuyerIdentity(context, null),
});

getVerifiedCartId must resolve a cart ID from protected server-side ownership state, not an unsigned client-controlled cookie or request value.

What this changes

  • Adds the current usable customer token to new cart buyer identity.
  • Marks checkout URLs in authenticated cart GET responses with logged_in=true.
  • Adds Customer Account lifecycle hooks that run before session commit. Authentication receives the exact issued token; refresh receives a discriminated authenticated, transient, or unauthenticated result.
  • Commits session state and returns a sanitized server error when a lifecycle hook rejects.
  • Adds secure cart synchronization guidance based on protected server-side cart ownership.

Developer impact

Includes a minor changeset for @shopify/hydrogen. The new cart option, lifecycle hooks, handler types, and documentation are additive; existing handler configuration remains unchanged. Token-bearing hooks require the session returned by createCustomerSession; custom session implementations can continue using standard routes and onLogout.

Out of scope

  • Hydrogen does not automatically associate existing carts during Customer Account lifecycle routes.
  • Applications own cart-ID verification, synchronization mutations, and fail-safe logout cleanup.

Risk

  • Customer-aware cart reads add one protected session lookup.
  • Lifecycle hooks run on the authentication request path, so applications should bound downstream work and honour the request abort signal.
  • Existing-cart synchronization must never trust an unsigned client-controlled cart ID.

@fredericoo fredericoo self-assigned this Aug 4, 2026
Comment on lines +101 to +103
onAuthenticated: attachCartBuyerIdentity,
onTokenRefresh: refreshCartBuyerIdentity,
onLogout: clearCartBuyerIdentity,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that we dont do it automatically as the use case is not for every customer.

in addition, we dont want to control how users store the cart id cookie and binding a non httpOnly cookie to a httpOnly session was flagged by security review from one of my subagents

this way we leave the responsibility to the implementer

this may be bad, because there is a lot more wiring up to do, im willing to discuss whether this is the most sane approach or an "auto sync" is a better idea

@fredericoo
fredericoo marked this pull request as ready for review August 5, 2026 14:10
@fredericoo
fredericoo requested a review from a team as a code owner August 5, 2026 14:10

@graygilmore graygilmore left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure how to 🎩 this one so I'm leaving the one critical thing that the bots found.

Comment thread packages/hydrogen/src/customer-account/session.ts Outdated

@frandiox frandiox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! I like the direction but at the same time I feel we should make things a bit simpler for the most common path? 🤔

What if we provide something like this? (name tbd)

const cartBuyerIdentity = createCartBuyerIdentitySync({
  customerSession,
});

const cartHandlers = createCartServerHandlers({
  cartBuyerIdentity,
  // Or if we want to have top level hooks:
  // onCartCreated: cartBuyerIdentity.onCartCreated,
});

const customerAccountHandlers = createCustomerAccountServerHandlers({
  cartBuyerIdentity
  // Or with hooks:
  // ...cartBuyerIdentity.customerAccountHooks,
});

Hydrogen already writes the cart cookie and receives the trusted cart ID directly from the Storefront API when it creates the cart. The helper would save that same ID in protected session storage and commit it on the response. It would not trust a cart ID sent by the browser.

createCartBuyerIdentitySync would run cartBuyerIdentityUpdate after login and refresh, clear the identity on logout, and own cookie cleanup and request ordering. The helper is opt-in and tree-shakeable.

We could still consider keeping the the lifecycle hooks if we want to be extra flexible as a low-level API and this helper can use it. If not, the helper can keep that lifecycle wiring internal and expose only the cart-specific API.

Comment thread packages/hydrogen/skills/hydrogen-customer-account/references/cart-sync.md Outdated
Comment thread packages/hydrogen/skills/hydrogen-customer-account/references/cart-sync.md Outdated
Comment thread packages/hydrogen/README.md Outdated
Comment thread packages/hydrogen/skills/hydrogen-customer-account/references/cart-sync.md Outdated
Comment thread packages/hydrogen/src/customer-account/session.ts Outdated
Comment thread packages/hydrogen/src/customer-account/session.ts Outdated
@fredericoo fredericoo added the gsd:50917 New Hydrogen label Aug 6, 2026
@fredericoo

Copy link
Copy Markdown
Contributor Author

Thanks for working on this! I like the direction but at the same time I feel we should make things a bit simpler for the most common path? 🤔

What if we provide something like this? (name tbd)

const cartBuyerIdentity = createCartBuyerIdentitySync({
  customerSession,
});

const cartHandlers = createCartServerHandlers({
  cartBuyerIdentity,
  // Or if we want to have top level hooks:
  // onCartCreated: cartBuyerIdentity.onCartCreated,
});

const customerAccountHandlers = createCustomerAccountServerHandlers({
  cartBuyerIdentity
  // Or with hooks:
  // ...cartBuyerIdentity.customerAccountHooks,
});

Hydrogen already writes the cart cookie and receives the trusted cart ID directly from the Storefront API when it creates the cart. The helper would save that same ID in protected session storage and commit it on the response. It would not trust a cart ID sent by the browser.

createCartBuyerIdentitySync would run cartBuyerIdentityUpdate after login and refresh, clear the identity on logout, and own cookie cleanup and request ordering. The helper is opt-in and tree-shakeable.

We could still consider keeping the the lifecycle hooks if we want to be extra flexible as a low-level API and this helper can use it. If not, the helper can keep that lifecycle wiring internal and expose only the cart-specific API.

my original idea was on the lines of createCustomerAccountServerHandlers({cartServerHandlers}) this way we call cartServerHandlers.updateBuyerIdentity automatically whenever the user is authorised or logs out

a security subagent flagged that this could mean we attach a non httpOnly cookie that can be fiddled with by the user, to an encrypted user session. to work around that i exposed the hooks that make it possible to do it yourself, but it's now down to the implementer to keep the cart cookie safe however they want

i would now suggest we do it like the initial idea (binding customer account with cart handlers like my code example) and consider either encrypting the cart cookie or just live with it, it's not really a security concern, the cart id cookie is already exposed and does nothing, attaching a modified cart id to your session means nothing at all to us

it felt much simpler to operate in the initial implementatioin than it does not (surely a lot less docs)

Associate new carts with currently authenticated Customer Account sessions and expose lifecycle hooks for application-owned synchronization after authentication, refresh, and logout.

Keep existing-cart ownership verification outside Hydrogen, document protected server-side cart bindings and fail-safe logout cleanup, and preserve session state with sanitized lifecycle-hook failures.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde
The change adds public Customer Account lifecycle hooks and cart handler options, so consumers receive it as an additive minor release.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde
Expose the exact authorization token and a discriminated refresh result so integrations can synchronize cart buyer identity without rereading session storage or conflating transient and definitive refresh failures.

Restrict token-bearing hooks to factory-created sessions while preserving standard routes and logout hooks for custom CustomerSession implementations.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde
Replaces the public onAuthenticated/onTokenRefresh/onLogout lifecycle
hooks with a cartServerHandlers option: Hydrogen now owns the sync loop
(attach on authorize/refresh, detach on definitive rejection and logout).
Sync is best-effort and never blocks the route redirect; a failed detach
during logout expires the cart cookie as fail-safe.

Assisted-By: devx/5bed7c10-b34f-44d6-afc4-4a93ef26e5ca
Assisted-By: devx/48ff1d59-2f7a-4e3c-a981-4d1802189c64
…lers

Assisted-By: devx/48ff1d59-2f7a-4e3c-a981-4d1802189c64
@fredericoo
fredericoo force-pushed the fb-cart-customer-identity-sync branch from 19bfb33 to eaaaf9b Compare August 11, 2026 18:25
@fredericoo
fredericoo marked this pull request as draft August 11, 2026 18:25
@fredericoo
fredericoo marked this pull request as ready for review August 13, 2026 19:31

@frandiox frandiox left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version is much simpler to use 🙌

Comment thread packages/hydrogen/src/customer-account/session.ts Outdated
Comment thread packages/hydrogen/src/customer-account/session.ts
Comment thread packages/hydrogen/src/core/cart/server-handlers.ts Outdated
Comment thread packages/hydrogen/src/core/cart/server-handlers.ts
Assisted-By: devx/b81bba60-d341-4412-9485-da84600941e0
@fredericoo
fredericoo marked this pull request as draft August 17, 2026 18:10
…dentity-sync

# Conflicts:
#	packages/hydrogen/src/core/cart/queries.ts
#	packages/hydrogen/src/customer-account/session.ts

@andguy95 andguy95 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. I like we are inverting and passing the session in as needed.

Will possibly need this PR to update buyer and their local as well for country / currency switcher.

@fredericoo
fredericoo marked this pull request as ready for review August 18, 2026 11:25
@fredericoo fredericoo changed the title Add cart customer identity sync hooks Sync cart buyer identity when auth status changes Aug 18, 2026
@fredericoo
fredericoo merged commit 2948455 into preview Aug 18, 2026
5 checks passed
@fredericoo
fredericoo deleted the fb-cart-customer-identity-sync branch August 18, 2026 11:26
IgorOffline pushed a commit to IgorOffline/Shopify-hydrogen that referenced this pull request Oct 10, 2026
* feat: sync cart customer identity

Associate new carts with currently authenticated Customer Account sessions and expose lifecycle hooks for application-owned synchronization after authentication, refresh, and logout.

Keep existing-cart ownership verification outside Hydrogen, document protected server-side cart bindings and fail-safe logout cleanup, and preserve session state with sanitized lifecycle-hook failures.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde

* chore: mark cart identity release as minor

The change adds public Customer Account lifecycle hooks and cart handler options, so consumers receive it as an additive minor release.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde

* feat: pass tokens to customer lifecycle hooks

Expose the exact authorization token and a discriminated refresh result so integrations can synchronize cart buyer identity without rereading session storage or conflating transient and definitive refresh failures.

Restrict token-bearing hooks to factory-created sessions while preserving standard routes and logout hooks for custom CustomerSession implementations.

Assisted-By: devx/4da10d8d-5225-495f-beb2-40d5a959dcde

* feat: wire cart buyer identity sync into customer account handlers

Replaces the public onAuthenticated/onTokenRefresh/onLogout lifecycle
hooks with a cartServerHandlers option: Hydrogen now owns the sync loop
(attach on authorize/refresh, detach on definitive rejection and logout).
Sync is best-effort and never blocks the route redirect; a failed detach
during logout expires the cart cookie as fail-safe.

Assisted-By: devx/5bed7c10-b34f-44d6-afc4-4a93ef26e5ca

* fixup! feat: sync cart customer identity

Assisted-By: devx/48ff1d59-2f7a-4e3c-a981-4d1802189c64

* fixup! feat: wire cart buyer identity sync into customer account handlers

Assisted-By: devx/48ff1d59-2f7a-4e3c-a981-4d1802189c64

* fix: harden cart customer identity sync

Assisted-By: devx/b81bba60-d341-4412-9485-da84600941e0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants