1CHANNELS
10TABLES
D0–D3GATES
ALL GATES ARMED
--:--:--
中文
M3 · minimal closed loop shipped

Run a self-operated store
as one fail-closed commerce loop

Product center, content, listing, orders and after-sales as one minimal loop. A pluggable ChannelAdapter replays an offline sandbox channel; live channels stay fail-closed until credentials exist. Every amount and external move — publishing, refunds — is decided by the zeus kernel over HTTP and waits on human approval. Orders are idempotent, every key action is audited, and nothing unbuilt ever fakes success.

1 complete closed loop48 tests green12 API routes
OPERATIONS CONSOLE1 CHANNEL · OFFLINE SANDBOX
ZEUS DECISION KERNEL · HTTPpublish and refund are graded here, never locally
D3 publish  HELD 423 → APPROVED → channel.upsert
D2 refund   HELD 423 → APPROVED → channel.refund
kernel down  503 DECISION_UNAVAILABLE · fail-closed
CHANNEL REGISTRYnew platform = one more adapter
sandbox-douyin  ONLINE · offline replay
douyin live      FAIL-CLOSED · 422 CREDENTIAL_MISSING
AUDIT LEDGER · commerce_eventsevery key action, replayable
listing.published  decision=D3 sku=SKU-DEMO-001
order.imported    ext=DY-SB-20261022-0001 1299 CNY idempotent=hit
refund.succeeded   decision=D2 ref=DY-SB-REFUND-5001
goods / order flowdecision gate · zeuschannel adapterclosed loopfail-closedstatic ops console diagram
the closed loop

How one order travels the whole loop

Five stations from product on file to refund settled. Content and channels sit behind ports, and the two money / external moves — listing and refund — pause at a zeus decision gate until a human approves. Nothing jumps the gate.

  1. 01
    PRODUCT

    Product on file

    The category and SPU/SKU center validates title, price, currency and skuCode uniqueness before anything moves downstream.

  2. 02
    CONTENT

    Content generated

    M3 uses a deterministic offline content stub — banned-word block, a single review, never auto-confirm. M4 swaps in the loom content base behind the same ContentClient port; callers do not change.

  3. 03D3 GATE
    LISTING

    Listing gated

    Content goes to zeus for a D3 external-action decision and returns 423 DECISION_HELD. It reaches channel upsert/publish only after human approval.

  4. 04
    ORDER

    Order pulled back

    The channel returns orders and the order center stores them, keyed by (channel, external_order_id) for idempotency, with SKU codes backfilled and order.imported written to the ledger.

  5. 05D2 GATE
    REFUND

    Refund gated

    Refund amounts are computed from order lines only. A D2 decision holds the request; the state machine runs requested → awaiting_approval → succeeded/rejected, and the channel refund fires only on approval.

domain modules

One codebase, nine focused modules

Each module owns one part of the commerce loop and talks to the rest through explicit ports. The channel, decision and content seams are contracts, so a live channel, the loom content base or a different store all plug in without rewriting the domain.

PRODUCT

Product center

Categories, SPU/SKU and attribute validation. A SKU is filed only when its code is unique and its price and currency are valid.

CONTENT

Content client

The ContentClient port ships a deterministic offline stub in M3 — banned-word block, a single review, never auto-confirm. M4 swaps in the loom content base without changing callers.

CHANNEL

Channel adapters

A five-method ChannelAdapter contract plus a registry — a new platform is just another adapter. The offline sandbox replays recordings; live channels fail-closed until credentials exist.

ORDER

Order center

Pulls orders from the channel and stores them locally. (channel, external_order_id) is idempotent, so replays never double-create orders.

DECISION

Decision port

A DecisionPort hands every amount and external action to zeus over HTTP via zeus-client. There is no local grading; an unreachable zeus returns 503.

PUBLISHING

Publishing orchestration

Content → D3 decision → human approval → channel upsert/publish. Every step of the external move happens in explicit order.

AFTERSALE

Refund state machine

requested → awaiting_approval → succeeded/rejected. Rejections are recorded and the channel refund fires only on approval.

FULFILLMENT

Fulfillment & payment placeholders

Inventory is maintained by hand; shipping and payment explicitly return 501 NOT_IMPLEMENTED_PLACEHOLDER in M3 instead of faking success.

AUDIT

Audit ledger

commerce_events records every key action — decision requests, listings, order imports and refund transitions — replayable and traceable.

engineering guarantees

Designed to fail-closed, not to fail-open

Commerce moves money and triggers real external actions. Commerce OS would rather refuse and return a precise error than perform something it cannot authorize. These four guarantees are enforced by the code and its tests.

FAIL-CLOSED1

Fail-closed by default

Unregistered channels are refused; a live channel without credentialRef returns 422 CREDENTIAL_MISSING; an unreachable zeus returns 503 DECISION_UNAVAILABLE. No amount or external move ever happens without authorization.

DECISION EXTERNALIZED2

Grading is externalized to zeus

The repo never grades locally. Publish (D3), refund (D2) and every other money or external move go to the decision kernel over HTTP; D3/D2 return 423 DECISION_HELD and wait for human approval.

NO FAKE SUCCESS3

Unbuilt paths return 501, not fake success

Payment and fulfillment are explicit M3 placeholders returning 501 NOT_IMPLEMENTED_PLACEHOLDER; content is an offline stub, not a model. Placeholders are a stated product stance, not silent mocks.

IDEMPOTENT & AUDITED4

Idempotent stores, full audit trail

Orders are deduplicated by (channel, external_order_id) for safe replay. Decision requests, listings, order imports and refund transitions all land in commerce_events, matching zeus decision records one to one.

architecture

Ports-oriented layering; dialect lives only in storage

HTTP v1 routes, a composition root, domain modules and three ports. Storage speaks both SQLite and PostgreSQL, with dialect confined to the storage layer and paired, numbered migrations.

L1

Runtime & quality gates

Node ≥22, TypeScript strict (noUncheckedIndexedAccess, exactOptionalPropertyTypes), 48 Vitest cases including a real zeus-client path through a fake HTTP zeus and the GATE-M3 end-to-end loop.

L2

Dual-dialect storage

Bundled node:sqlite locally with zero dependencies, PostgreSQL in production; sqlite/pg migrations are paired by number and enforced by check:migrations.

L3

Three ports

ChannelAdapter (five methods), DecisionPort (zeus-client), ContentClient (M3 stub → M4 loom).

L4

Domain modules

storage · channel · product · order · decision · content · publishing · aftersale · fulfillment.

L5

Composition root · services.ts

Injects the DecisionPort, ContentClient and channel registry at assembly; domain modules depend only on ports.

L6

HTTP v1 · Hono

12 routes with one unified error envelope { error: { code, message, retryable, details? } }; the code-to-HTTP mapping is centralized.

DEPLOY
open source · self-host

Spin the whole loop up on your machine in minutes

No live credentials required: the sandbox channel replays recorded fixtures, a fake-zeus decision path and the GATE-M3 end-to-end test prove the loop, and SQLite is the default with PostgreSQL ready for production.

  • 48 tests green — a real zeus-client through a fake HTTP zeus for the full decision path, plus the GATE-M3 end-to-end loop.
  • SQLite by default via bundled node:sqlite; PostgreSQL for production, with paired dual-dialect migrations.
  • The sandbox channel replays offline — no network, no keys. A 1299 CNY demo order runs through listing, pullback and refund.
  • Live channels, model keys, payment and fulfillment stay fail-closed / 501 until you wire them — never faking success.
QUICKSTART:8787
# clone + install
$ git clone https://github.com/bayernjf/commerce-os.git commerce-os
$ cd commerce-os && npm install
# migrate the local SQLite store
$ npm run migrate:up
# start — http://127.0.0.1:8787
$ npm start

M3 milestone artifact. Live channels need qualifications and credentials; M4 content comes from the loom base.