Commerce package

The marketplace kernel — what belongs in it, what must stay out, and the file shape inside

Features ask. Commerce decides. Workflows orchestrate. The database stores.

packages/commerce is the marketplace engine: the rules, the state machines, and the guarded writes for everything the marketplace has an opinion on — orders, listings, payments, fulfillments, payouts, disputes, offers, wallet, onboarding.

The dependency direction

apps/web → packages/features → packages/commerce → packages/db

Strictly one way. Commerce never imports a feature.

LayerOwns
packages/featuresServer actions, page reads, components, views, cache invalidation, effects (emails, notifications, sockets)
packages/commerceState machines, transition rules, money math, market config, the protected writes

What must stay out of commerce

Anything that imports next/*, @repo/delivery, @repo/payments, a provider SDK, @repo/ui-web, @repo/internationalization, or an href. And no "use server" — server actions are features/<feature>/actions/.

The pure parts (machines, models, view-state, diagrams) import only xstate and @repo/db types, so a client component can import them to gate UI without dragging in the framework or a durable runtime. That is the property the boundary protects.

The structural rule

No bare db.update(<protected_table>) and no bare db.insert(<protected_table>) outside commerce/<entity>/, for orders, listings, payments, fulfillments, payouts, disputes, user balances, offers, refunds and balance transactions.

Status, balances and an order's money columns go through the entity's service; a row is created by its repository (insertOrder, insertListing, insertOffer…). The user_balance_transactions ledger is append-only — only wallet.events.insertBalanceEvent writes there.

@repo/oxlint-plugin (cute/no-protected-write, wired in oxlint.config.ts) enforces this. A commit that renames a protected column or adds a protected table updates that plugin's tables in the same commit; its tests in packages/oxlint-plugin/__tests__ are where the shape is pinned.

Layout

Sibling folders per domain, flat files inside — no domain/, application/ or services/ subfolders. Files are dotted: order.machine.ts, never order-machine.ts.

FileOwns
*.model.tsWhat the thing is — status unions, row shape, transition and result types
*.machine.tsThe pure machine. No DB, no provider, no Restate
*.view-state.tsPure intent derivation for UI (getOrderPrimaryAction)
*.service.tsThe transition workflow. Takes a tx, writes via the repository, logs the event. The authoritative protected write
*.repository.tsDB primitives the service uses — find, atomic update, optimistic lock
*.events.tsThe append-only event-log writer
*.diagrams.tsMermaid strings, consumed by these docs and the simulator

Domains: orders, listings, payments, fulfillments, offers, payouts, refunds, wallet, onboarding, identity, invoices, sellers, reviews, boosts, plus the cross-cutting markets, config, pricing, money, transitions.

Two machine shapes

Orders + payments + fulfillment + disputes are one XState v5 parallel machine (orders/order.machine.ts) — four concurrent regions that advance together when a single event fires. A dispute is not its own machine; it is a region.

That file declares its side effects as types with throwing placeholders, so it imports only xstate. The durable runtime lives in @repo/workflows: order.restate.ts injects the real implementations via orderMachine.provide({ actors }) and hands the result to Restate as a virtual object keyed by orderId. Each effect commits through this package's atomic transitionOrder. The machine orchestrates; Postgres stays the truth.

Everything else — listings, offers, payouts, messages, wallet — stays on the lighter TransitionDef[] rule tables. Don't migrate them to XState without a real parallel-region or interrupt reason.

Scale

Cüte is Vinted-class: a C2C marketplace with a simple lifecycle (paid → shipped → delivered, plus disputes, held funds and payouts). It is not Shopify-class — no variants, no subscriptions, no multi-warehouse, no tax engine. Vendure / commercetools / Medusa patterns are sized for catalogue ecommerce. Don't reach for them.

Do not: reshape into domain/ + application/; add a second payments or delivery port (there is exactly one of each); adopt Inngest, Temporal or Trigger.dev (Restate owns durability); rename orders to transactions.