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 |
|---|---|---|
|
Anonymous visitors and guest carts |
Controls hosted visitor session TTL, GraphQL visitor session TTL, and guest-cart row expiry. |
|
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.
Consent#
- 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.
Building the consent banner#
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.
{% 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.