Guest checkout
Anonymous visitors shop with a signed guest session; the storefront runs guest checkout itself.
Guest carts use the Shop API; guest session creation, guest checkout, the private receipt and order lookup run in the storefront.
Guests have no user token. For cart writes the webshop authenticates as the storefront with a client-credentials
token and passes the visitor’s guest session alongside it. It calls the API through the typed client
(callAsGuest((api, options) => api.cart.items.add(input, options), { session: 'required' })), which sends the token,
Guest-Session and Shopper-IP as headers. Everything else a guest does runs in the storefront through
shopOperation, against the shop data, with the visitor’s address from cf-connecting-ip.
Guest checkout, bank-challenge completion, private receipt links and number/email lookup run in the storefront. They are absent from the Shop API and from agent tool discovery.
Private receipt and bank challenge
Checkout returns the order and its private accessToken once. The webshop retains that validated credential in
the HttpOnly fh_guest_order_<id> cookie and removes it from the receipt URL. The storefront reads the
order with it directly, so the token never travels in a URL or log of another service. The bank-challenge return page
settles in the storefront with the same capability (the link’s token or the cookie); a wrong or missing token is “not
found” and settles nothing.
After a paid or pending challenge, the webshop remembers the token and redirects to /orders/<id>.
Failed payments return to checkout. The browser publishable key comes from the webshop.
The number/email lookup (a POST because the email is personal data) uses the domain throttle, throttleGuestLookup:
five lookups per client per minute, and five misses per email per hour / fifteen per day across clients. A refusal shows
the private-link message. Lookup returns only receipt status, totals and line titles.
One signed guest cookie
The webshop reads only fh_guest, an HttpOnly, SameSite=Lax, path /, thirty-day cookie that is Secure in
production. New carts are minted by the storefront, from server-only code. The storefront verifies the signature on every read, so a
forged or expired cookie is dropped and names no cart. An unsigned fh_cart cookie from older builds is ignored and
never recovered or merged. A guest checkout places an order per request; a second one after a paid order finds the cart empty and is refused. Sign-in merges
the cart from the verified Guest-Session and clears the cookie. A rejected capability is discarded; a write can mint
a new one. Ended agent-browser carts render empty and cannot be written.
Guest rate limits
| Operation | Runs in | Guest cart budget | Visitor address budget |
|---|---|---|---|
| guest session creation | storefront | — | 20/minute |
addCartItem, updateCartItem, removeCartItem |
Shop API | 60/minute | 300/minute |
| guest checkout | storefront | 10/10 minutes | 30/10 minutes |
| order lookup | storefront | — | 5/minute (+ per email) |
Both Workers keep exact fixed-window counters in the same rate_limits D1 table, each under its own keys (the
storefront’s minting and checkout limits use storefront: keys, the lookup throttle guest_lookup: keys). The address is
Cloudflare’s connecting IP (an IPv6 address counts as its /64); a missing one uses the shared unknown bucket. The Shop
API’s 429 rate_limited includes Retry-After in seconds; the storefront shows the refusal on the page. Customer
callers have no guest rules.
Security notes
The storefront holds the guest session key, so it can mint a capability for any cart id. That is no worse than its
database binding, which can read any cart, and guessing a 122-bit id remains infeasible. The minting code is imported only
from server-only modules (check-packages enforces it), and minting is limited per address. On cart writes the Shop API
trusts the visitor IP header only from the storefront client, and only for rate limiting.
Auth0 meters client-credentials tokens, so the webshop caches the token per isolate and in the Workers cache until 60 seconds before expiry.