Skip to content
Tarn & Timber Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

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.

Was this page helpful?