Sessions, carts, auth, and consent

How CTX, hosted storefronts, and Storefront GraphQL share visitor sessions, guest carts, customer auth, and consent.

Sessions, carts, auth, and consent#

CTX and Storefront GraphQL share the same visitor-session foundation. Hosted storefront visitors receive a secure _session cookie; headless clients create the same visitor session through GraphQL and present its token on later requests.

Admin settings#

Setting

Scope

Behavior

guest_cart_duration_days

Anonymous visitors and guest carts

Controls hosted visitor session TTL, GraphQL visitor session TTL, and guest-cart row expiry.

session_duration_days

Hosted logged-in customer sessions

Controls customer-session validity for hosted CTX and hosted GraphQL session auth.

Both values are constrained to 1-90 days. Saving the settings is transactional for the settings row and the audit record: either both commit or neither does.

Visitor sessions#

  • Hosted storefront visitors receive `_session` on first visit.
  • Headless clients call `visitorSessionCreate` and then send the visitor session token on GraphQL requests.
  • Visitor sessions use `guest_cart_duration_days` as a sliding TTL.
  • Existing anonymous sessions are lazily re-touched to the current setting on the next hosted request or GraphQL renewal.

Guest carts#

A guest cart is a cart whose owner is the visitor session token. GraphQL cart writes for guest owners pass guest_cart_duration_days to edgeservice, so guest-cart expiry follows the admin setting.

Hosted customer sessions#

Hosted customer login or registration creates a customer session and binds the edge session to that customer. The edge session is extended to session_duration_days at bind time.

  • CTX does not load `ctx.Customer` directly from the edge session customer id.
  • CTX asks storefrontservice whether the hosted customer session is still valid.
  • Hosted GraphQL uses the same storefrontservice check before treating an internal hosted session as customer-authenticated.
  • Effective hosted customer-session deadline is `customer_sessions.created_at + session_duration_days`.
  • If expired, the customer session is deleted and the edge customer binding is cleared. If valid, activity and expiry are refreshed to the current effective deadline.

Customer access tokens#

Headless customer identity is token-based. Customer access tokens keep the expiry they were issued with. Changing session_duration_days does not rewrite already issued customer access token expiries.

B2B GraphQL buyer context must be explicit: a customer access token plus company-location context. A public visitor session or hosted customer session is not enough to infer B2B company, catalog, price list, or currency.

  • Anonymous consent is stored on the edge visitor session under the `consent` key.
  • Consent writes require a real visitor session.
  • On customer login or registration, stored session consent is merged into the customer record.
  • Consent merge does not invent consent. It only copies consent the visitor explicitly stored on the session.

Do not do these#

  • Do not hard-code a 7-day visitor or session TTL in hosted CTX or GraphQL paths.
  • Do not load customer identity from edge `customer_id` without storefrontservice validation.
  • Do not apply guest cart duration to customer-owned carts.
  • Do not rewrite existing customer access token expiries when settings change.

The consent banner is a theme section — the platform never injects one. Target it at global_overlay, declare consent and csrf_token in ctx_needs, and read ctx.consent: required tells you this visitor should be asked (banner enabled and the visitor is inside the enforced countries — resolved server-side, never by your template), given tells you they already chose, and categories lists what they granted. Categories are platform-defined: essential, analytics, marketing, personalization. Render the choice controls for the last three; essential is always on.

vein
{% if consent.required %}
<div class="my-banner{% if consent.given %} my-banner--hidden{% endif %}" data-my-consent>
  {% form 'consent_give' %}
    <label><input type="checkbox" name="categories" value="analytics" checked> Analytics</label>
    <label><input type="checkbox" name="categories" value="marketing" checked> Marketing</label>
    <label><input type="checkbox" name="categories" value="personalization" checked> Personalization</label>
    <button type="submit">Save choices</button>
    <button type="submit" name="accept_all" value="1">Accept all</button>
  {% endform %}
  {% form 'consent_give' %}
    <button type="submit">Necessary only</button>
  {% endform %}
</div>
{% endif %}

Keep the necessary-only button in its own form. A named submit button does not uncheck the checkboxes beside it, so a shared form would still post the checked categories. Both forms post to the consent_give action; consent_withdraw records an explicit no-consent choice. The grant's timestamp and country are stamped server-side, and unknown categories are refused — your banner paints the door, the platform enforces it.

Offer a way back in. Render the banner whenever consent.required (hidden once given) and reopen it from a footer "Cookie preferences" control — preselect the checkboxes from consent.categories so a returning visitor sees their actual choices. In the theme editor the preview always shows the banner as required-and-unanswered so the merchant can design it; on the live storefront the real decision applies.