Checkout sessions
A checkout session is the buyer-facing wizard that culminates in an order. The row in checkout_sessions is short-lived (each session has an expires_at) and holds the buyer's snapshot of delivery method, payment method, and money breakdown until the order row gets created.
Unlike orders, listings and disputes, the session is not a state machine — it has no event log table, no transition workflow, and no actor-permission rules. The status is just the wizard step the buyer is on, plus two terminal states for "checkout happened" and "buyer abandoned."
Statuses
flowchart LR
delivery --> payment --> review --> completed
delivery -.expires_at hit.-> expired
payment -.expires_at hit.-> expired
review -.expires_at hit.-> expired| Status | Meaning |
|---|---|
delivery | Buyer picking delivery method + address |
payment | Buyer picking payment method (PIX / credit card / balance) |
review | Final review screen before placing the order |
completed | Order created: the checkout workflow (packages/workflows/src/checkout/checkout.object.ts) flips this once the order is persisted, whichever transport ran runCheckout (web's /checkout, the app's /checkout/[sessionId]/pay on cute-api) |
expired | expires_at passed without completion; expiry is lazy-on-read today (packages/features/checkout/queries/get-checkout-session.query.ts) |
Expiry today
Sessions expire lazily: when get-checkout-session.query.ts reads a row past its expires_at it flips the row to expired in-place. Support reading the table directly will see stale delivery / payment / review rows until that read happens. An eager sweep would keep the table truthful for admin and support tools; it does not exist yet.
Why no FSM
The transitions are linear, no actor-permission gating beyond "session owner can act on their own session" (which is RLS, not FSM), and the row is throwaway once the order exists. The order's checkout_session_id FK preserves the link for audit.