Appearance
Commerce internals
Chapter 15. Chapter 6 gave the commerce seams; this chapter traces the interactive paths in full — the cart cookie protocol, the checkout handshake, and login.
The per-shopper, no-store interactive paths — cart, checkout, login, account — that live outside the cached bundle render. Code: apps/origin/src/handlers/{cart,account}.ts, apps/origin/src/index.ts (checkout + island router), packages/builder-core/src/storefront/{cart,kwikpass-session}.ts, packages/storefront-integrations/*.
Add to cart — handleCart
POST /cart/add: build the backend (cartBackendFor → buildCustomClient; null when unconnected → empty cart, no mutation), read the httpOnly rt_cart token, then:
- Variant resolution — a PDP posts a real
variantId; a grid/collection card posts ahandle, soresolveVariant(handle)fetches the product and takesvariants[0](returns''on failure → the add is skipped, never a bogus id). (Grid add always adds the first variant — no size/colour choice server-side.) - Mutation —
CartService.add(token, lines)(creates a cart first if there's no token — the backend keys carts by token); the returned cart's.idis the token to persist. - Cookies — on success, set the httpOnly server cart cookie, then whichever mirror the active cart provider contributes (below).
POST /cart/update sets/removes a line; GoKwik's updateCart replaces the whole quantity set, so it re-sends every line with only the target changed, and removal keys by variant_id, not line id. When the cart empties it appends an expired-presence cookie so the native runtime stops fetching /cart.
Three cookies, three owners:
| Cookie | Flags | Set by | Read by | Purpose |
|---|---|---|---|---|
rt_cart | HttpOnly · Lax · Secure · 30d | origin | server only | the real cart token (bearer) |
X-Cart-Token | Lax · Secure · not HttpOnly | side-cart | GoKwik widget JS | JS-readable mirror for the drawer |
rt_cart_present | Lax · Secure · not HttpOnly · value "1" | native cart | native runtime | presence marker (skip /cart fetch) |
The response is three-way negotiated: x-ratio-cart header → a re-rendered drawer fragment; Accept: json → JSON; else a 303 to backToReferer — the referring path only (never the raw Referer, always same-origin), excluding /cart exactly (so /cartier still bounces).
Checkout
POST /checkout is exempt from the reserved-swallow and reachable without a session (guest checkout). It calls CartService.createCheckout(token) → { merchantCheckoutId } (empty id on error → the client shows "unavailable", never a 500). The client script then:
triggerRatioSideCartCheckout()→fetch('/checkout')→gokwikSdk.initCheckout({...merchantCheckoutId...}).- A completion listener on
window'message', origin-pinned tohttps://*.gokwik.co/.io(apostMessagecarries a forgeable origin, so it must be pinned), gated toeventName === 'Purchase', once-guarded. It stashes the order insessionStorage, clears the cart (localStorage token +X-Cart-Tokencookie), and redirects to/order-confirmation?id=…, which hydrates from the stash (every field HTML-escaped).
event_id is the order name
There's no distinct Purchase event_id — the id that flows through is cartData.orderName (e.g. #1001), used for both the stash and the ?id= param.
Money units flip here
Everywhere else the server works in paise (the money filter divides by 100). But the client checkout/hydrate script treats cartData.*.price as rupees (₹ + toFixed(2)). Don't carry the server's paise assumption into the order-confirmation/checkout script, or amounts render 100× off.
Login (KwikPass)
Three defer scripts in order: our content-addressed bootstrap asset (/assets/kwikpass.<hash>.js), the GoKwik CUSTOM-platform core bundle (not the Shopify one), and the merchant plugin. defer (not async) because ours must set window.merchantInfo (a merge, since side-cart assigns it during parse) before the core runs.
Why an asset, not inline (the CSP knot)
The PWA is default-on → every page carries a hashed SW-registration snippet → that hash in script-src makes 'unsafe-inline' inert (CSP3). An inline bootstrap would be blocked; authorizing it by hash on the shared GOKWIK_CSP would then make 'unsafe-inline' inert on the order page too (which has no PWA hash but needs inline for side-cart/checkout). 'self' already covers a same-origin external script, so an asset strands nothing.
GoKwik announces success on the CUSTOM platform via a same-page user-loggedin CustomEvent (not a postMessage — so no origin check exists or is needed; it's only a re-fetch hint, the cookie is authoritative). The handler writes the JS-readable rt_kp_at access-token cookie and calls islands.refresh('account'). Server-side, KWIKPASS_COOKIE_NAMES (four names, one JWE we can't read — presence is the login signal) is distinct from rt_kp_at (a real bearer the origin forwards to /os + /cs).
Account — renderAccountResponse
Gated upstream: a GET under /account with no KwikPass cookie → 302 to / (bare, no-store), so the handler only runs signed-in. It resolves the account provider (a non-native id logs account_provider_unhosted and fails safe to native), then fetches in parallel: profile (/cs), order history (/os), and an optional order detail. Addresses key on gk-phone; if the profile has none, a minimal 1-order fetch recovers the phone first. Orders go through the @shopkit/data-layer client (getOrderHistory/getOrderById — scoped to the token's customer, not an IDOR); profile + addresses are plain REST against /cs (the data-layer client doesn't model them). Amounts are paise passthrough (paiseAmount rounds, never ×100). Render is a 3-tier fallback: templates/account.json → sections/main-account.liquid → a built-in, all no-store, private.
The GoKwik client & the provider seam
buildCustomClient wires createCommerceClient({ type:'custom', services:{ product, cart, order }}). The service prefixes are env base URLs at the GoKwik gateway — /ps product, /os cart and order, /cs customer, /pi/pc nav — onto which the adapter appends fixed endpoint paths. The custom adapter throws for much of ICommerceClient (getCustomer*, getOrder/createOrder/… are "not implemented"); only order history/detail are real, and customer profile/addresses bypass the adapter entirely.
Providers resolve per capability: explicit store pin if enabled+usable → else the first usable vendor by registry priority → else a first-party native backstop. CAPABILITY_ORDER is cart, checkout, login, account, search — cart before login on purpose (cart assigns window.merchantInfo, login merges into it). CSP is unioned by mergeCsp, honoring the 'unsafe-inline'-goes-inert-when-a-hash-is-present rule (CSP2+). checkoutPathHealth reports ready / off / partial (drawer opens but Checkout silently does nothing — one env set, the other not).
Gotchas
event_idisorderName, not a distinct id.- postMessage (checkout) is origin-pinned; the CustomEvent (login) is not — different trust models, deliberately.
- Two
CartServiceclasses — our thin server wrapper vs the data-layer's internal one; don't conflate. - The custom adapter throws for customer/order-CRUD — profile/addresses are plain
/csREST. - Removal keys by variant_id;
setQuantityre-sends the whole line set (GoKwik replaces, not patches). - Paise passthrough on the server, but the checkout hydrate script treats amounts as rupees — different unit conventions between the two paths; verify against the live payload.
/osserves both cart and order in prod; sandbox differs.resolveVariantpicksvariants[0]blindly — no server-side variant selection on grid adds.- The account provider seam is a stub-for-future — only native is implemented.