Skip to content

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 (cartBackendForbuildCustomClient; 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 a handle, so resolveVariant(handle) fetches the product and takes variants[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.)
  • MutationCartService.add(token, lines) (creates a cart first if there's no token — the backend keys carts by token); the returned cart's .id is 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:

CookieFlagsSet byRead byPurpose
rt_cartHttpOnly · Lax · Secure · 30doriginserver onlythe real cart token (bearer)
X-Cart-TokenLax · Secure · not HttpOnlyside-cartGoKwik widget JSJS-readable mirror for the drawer
rt_cart_presentLax · Secure · not HttpOnly · value "1"native cartnative runtimepresence 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 to https://*.gokwik.co/.io (a postMessage carries a forgeable origin, so it must be pinned), gated to eventName === 'Purchase', once-guarded. It stashes the order in sessionStorage, clears the cart (localStorage token + X-Cart-Token cookie), 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.jsonsections/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, searchcart 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

  1. event_id is orderName, not a distinct id.
  2. postMessage (checkout) is origin-pinned; the CustomEvent (login) is not — different trust models, deliberately.
  3. Two CartService classes — our thin server wrapper vs the data-layer's internal one; don't conflate.
  4. The custom adapter throws for customer/order-CRUD — profile/addresses are plain /cs REST.
  5. Removal keys by variant_id; setQuantity re-sends the whole line set (GoKwik replaces, not patches).
  6. 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.
  7. /os serves both cart and order in prod; sandbox differs.
  8. resolveVariant picks variants[0] blindly — no server-side variant selection on grid adds.
  9. The account provider seam is a stub-for-future — only native is implemented.