Guest Checkout Module — Storefront
HTTP surface for guest (no-account) checkout — contact capture for an anonymous cart, a non-blocking "you already have an account" hint, and public order tracking after the order…
HTTP surface for guest (no-account) checkout — contact capture for an anonymous cart, a non-blocking "you already have an account" hint, and public order tracking after the order is placed. Delivered as a removable plugin built on better-auth's anonymous plugin: a guest is a throwaway anonymous user, so the core cart / place-order flow runs unchanged.
Source:
api-modules/guest-checkout/src/controllers/store-guest-checkout.controller.ts,api-modules/guest-checkout/src/controllers/store-guest-order-lookup.controller.ts.This is a customer-facing-only surface — no admin or vendor controller. Removing
GuestCheckoutModule.forRoot()fromapp.moduledisables every endpoint below, the guest confirmation email, and the guest→account conversion. The better-authPOST /auth/sign-in/anonymousendpoint remains but is inert without this module.
The guest flow
- Start a guest session —
POST /auth/sign-in/anonymous(better-auth, not this module). This mints an anonymoususerwhoseuser.emailis an unroutable temp address. - Build the cart — the standard
store/cartendpoints, using the samex-cart-tokenhandle. - Capture contact —
POST /store/guest/contactstores the guest's real email (used for the confirmation + tracking link) against the active cart. - Place the order — the standard
store/ordersplace-order flow, unchanged. - Confirmation — on
order.placed, this module mints an unguessable lookup token, binds it to the contact, and emails a tokenized order-status link. (The default order-placed notification is suppressed for anonymous users, so this is the only confirmation a guest receives.) - Track — the guest opens the tokenized link (
GET /store/guest/orders/:token) or uses the email + order-number form (POST /store/guest/orders/lookup). - (Optional) Convert — if the guest later signs up / signs in, better-auth links the anonymous account and this module re-keys their orders, addresses, and discount usage onto the real account before the anonymous user is deleted.
Conventions
Authentication
| Endpoint | Auth |
|---|---|
POST /store/guest/contact | anonymous session required (guard is OptionalAuth, but the handler 400s without session.user.id) |
GET /store/guest/account-exists | none (public) |
POST /store/guest/orders/lookup | none (public) |
GET /store/guest/orders/:token | none (public) |
All controllers run BetterAuthGuard with @OptionalAuth() — a session is read when present but never mandated by the guard. The order-tracking endpoints are deliberately public so a guest can track without ever logging in.
Headers (contact endpoint)
Mirrors the cart/checkout controllers; forwarded to the cart layer verbatim:
| Header | Direction | Notes |
|---|---|---|
x-cart-token | request (optional) + response | Opaque cart handle. The response always re-emits the active cart's token. |
x-platform | request (optional) | WEB / APP (case-insensitive). Defaults to WEB. |
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST (no guest session on contact), VALIDATION_ERROR |
| 404 | GUEST_ORDER_NOT_FOUND (order tracking; neutral — see below) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Neutral 404 on tracking
Both order-tracking endpoints raise the same 404 GUEST_ORDER_NOT_FOUND on any miss (unknown token, wrong email, unknown order number, email/order mismatch) so the endpoints can't be used to enumerate orders or probe which emails placed orders.
Domain types
GuestContactResponse — POST /store/guest/contact
type GuestContactResponse = {
email: string;
accountExists: boolean; // non-blocking hint; never gates checkout
};GuestAccountExistsResponse — GET /store/guest/account-exists
type GuestAccountExistsResponse = { exists: boolean };Order detail
The two order-tracking endpoints return the order module's OrderResponse shape (see order.md) — the same payload an authenticated customer gets for their own order.
Endpoints
POST /store/guest/contact — Capture the guest's contact details
Requires an anonymous session (step 1 above). Resolves the active cart from x-cart-token (minting one if absent), stores the contact email/name/phone against that cart, and returns a non-blocking accountExists hint. The response sets x-cart-token.
Headers — x-cart-token (optional), x-platform (optional).
Body
{ "email": "guest@example.com", "name": "Jane Doe", "phone": "+15551234567" }| Field | Type | Constraints |
|---|---|---|
email | string | Trimmed, valid email. The real contact address (the anonymous user.email is unroutable) |
name | string? | Trimmed, 1..255 chars |
phone | string? | Trimmed, 1..32 chars |
Response 200 — GuestContactResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | BAD_REQUEST | No guest session — "Start a guest session (POST /auth/sign-in/anonymous) before capturing contact details" |
| 400 | VALIDATION_ERROR | Body fails zod (bad email, etc.) |
GET /store/guest/account-exists — Does a registered account use this email?
Non-blocking lookup driving the "you already have an account — log in to keep your orders together" hint. Never blocks guest checkout.
Query
| Name | Type | Constraints |
|---|---|---|
email | string | Trimmed, valid email |
Response 200 — GuestAccountExistsResponse.
POST /store/guest/orders/lookup — Track by email + order number
The WooCommerce/Magento-style guest tracking form.
Body
{ "email": "guest@example.com", "orderNumber": "SC-100245" }| Field | Type | Constraints |
|---|---|---|
email | string | Trimmed, valid email |
orderNumber | string | Trimmed, min 1 char |
Response 200 — OrderResponse (see order.md).
Errors
| Status | Code | When |
|---|---|---|
| 404 | GUEST_ORDER_NOT_FOUND | No order with that number, or the email doesn't match the captured contact |
GET /store/guest/orders/:token — Track by private status-link token
The unguessable token (32 bytes of entropy → 43-char base64url) embedded in the confirmation email. Built into a URL of the form <STOREFRONT_URL>/order-status/<token>.
Path params
| Name | Notes |
|---|---|
token | Order-status lookup token from the confirmation email |
Response 200 — OrderResponse (see order.md).
Errors
| Status | Code | When |
|---|---|---|
| 404 | GUEST_ORDER_NOT_FOUND | Unknown token, or no order is bound to it yet |
Side effects (not direct HTTP)
These run off the event bus, not from a client call, but shape what the endpoints above return:
| Trigger | Behavior |
|---|---|
order.placed (guest order) | Mints the lookup token, binds it to the contact + order, and emails the tokenized status link to the captured contact address. Idempotent — a re-delivered event does not re-mint or re-send. Failures are swallowed (the order is already placed; the email + order-number path still works). |
guest_checkout.anon_account_linked (better-auth onLinkAccount) | Re-keys the guest's order, customer_address (forced to isDefault=false), discount_usage, order_event, and non-active cart rows onto the new account, in a single transaction that completes before better-auth deletes the anonymous user (so a failure aborts the deletion rather than orphaning data). Guest-earned reward points are intentionally not migrated in v1. |
Related modules
auth— the better-auth anonymous plugin mints the guest session; theonLinkAccounthook drives conversion.POST /auth/sign-in/anonymousis the prerequisite forPOST /store/guest/contact.cart—POST /store/guest/contactresolves the active cart viaCartServiceusing the samex-cart-token/x-platformhandles as the cart endpoints. Seecart.md.order— order tracking returns the order module'sOrderResponse; the confirmation flow listens onorder.placed. Seeorder.md.notifications— the default order-placed notification is suppressed for anonymous users (recipient resolver), leaving this module's guest confirmation email as the sole notice.
Global Scripts Module — Storefront
Public read surface that returns the enabled global scripts for one document slot. The storefront layout calls this once per slot (HEAD, BODY_START, BODY_END) at render time and…
Notifications Module — Storefront
HTTP surface for the customer mobile app to register and unregister its FCM device token for push notifications. Customer devices are tagged appKind: "customer" (distinct from the…