Open source (MIT), version 0.0.1. The WooCommerce-equivalent for EmDash, Cloudflare's TypeScript CMS.
The reference storefront running locally, with prices and stock served in-process by the Otta plugin — this is what the quick start below gives you.
Otta turns an EmDash site into a store. It is one deployable, and it ships as two parts:
- Otta plugin — a sandbox-clean EmDash plugin that owns all money and stock truth
in-process: catalog, inventory, cart, checkout, orders, customers, payments, tax,
shipping, discounts, entitlements, reporting, and webhooks, plus storefront routes,
content-sync hooks and an admin console (pricing & inventory, orders, reports,
settings) and x402 gating for digital goods. Commerce state lives in the host's
per-plugin document store (
ctx.storage) via the@otta-sh/store-emdashadapter — no separate service, no second database. Its only outbound egress isctx.http.fetch, gated bynetwork:request+allowedHosts. The CMS owns content; every commercial field lives in the plugin's store and is edited in the admin console (ADR-0018, ADR-0020). - The reference site (
sites/staging) — a default EmDash site with the plugin already registered, so there's something to actually run. It's the storefront in the screenshot above and what the quick start boots: product listing pages, cart, and the admin console. Treat it as the worked example to copy from when wiring Otta into your own site — it covers catalog + cart only today (see Status).
A full store on your laptop — no Cloudflare account, no deploy, no database to run. The site's D1 content database and R2 media bucket are emulated locally by the Astro Cloudflare adapter, and commerce runs in-process inside the same worker (the plugin owns cart, order and inventory state in em-dash plugin storage), so there is no separate service and no Postgres in the loop.
pnpm install
# 1. Storefront + admin.
pnpm --filter @otta-sh/site-staging devThen open the dev-only setup bypass, which claims the site and applies the full seed including three sample products:
http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
The seed creates the three sample products as CMS content only — prices and stock are commerce fields it does not touch — so give them some:
# 2. Price, stock and activate the demo products (second terminal).
# It reads the products' real ids from the CMS (matching the seed's slugs),
# then prices and stocks each one through the SITE's own admin API — the same
# route the Pricing & inventory page uses, so the site URL is all it needs.
SITE_URL=http://localhost:4321 \
pnpm dlx tsx@4 sites/staging/scripts/seed-demo-commerce.ts/products now renders a priced catalog and add-to-cart takes a real inventory hold. Open
Pricing & inventory in the admin to reprice, restock, or price a product of your own —
that page is the only place commercial fields are edited; the CMS owns the title,
description and images.
One thing to know: this storefront covers catalog + cart only — see Status.
To deploy this for free on Cloudflare Workers, follow
DEPLOYMENT.md §2.
- Product model = hybrid. Content (title, description, images, SEO, taxonomies)
lives in a native EmDash
productscollection; commercial data (price, SKU, stock, tax, shipping) lives in the plugin's own document store. Link key = the CMS contentid. - One database. Commerce truth and CMS content share the site's single D1 database:
content lives in the CMS's own tables, commerce lives in the host's per-plugin document
store (
ctx.storage), namespaced by plugin id and collection. They are not joined in SQL — the hybrid product model is joined in app code at render time. - Ports and adapters.
@otta-sh/domainis pure (no IO); the stores that implement its ports live in@otta-sh/store-emdash, which writes one document per aggregate to the host's per-plugin document store (ctx.storage) by compare-and-set — D1 in dev and in production, with a dialect harness that runs the same adapters against SQLite and Postgres in CI. The plugin composes those stores in-process, and the domain's contract suites are the spec they are held to (ADR-0019). - Pluggable payments. Stripe (async webhook) and x402 (HTTP-402 at the page layer)
behind one
PaymentGatewayinterface. - Deployment. One Worker and one D1 database: the EmDash site with the plugin
registered trusted (in-process), on the Cloudflare Workers free plan, with cron
sweeps for cart/reservation expiry. The plugin still passes the full workerd sandbox
suite on every CI run, which is the binding contract (ADR-0006).
Step-by-step bootstrap guide:
DEPLOYMENT.md.
| Package | What it is |
|---|---|
@otta-sh/domain |
Pure ports, use-cases, branded money types, contract-test suites. No IO. |
@otta-sh/store-emdash |
Store adapters over the host's per-plugin document store — one document per aggregate, compare-and-set writes. |
@otta-sh/payments-stripe |
Stripe PaymentGateway adapter (async-webhook, raw-body HMAC). |
@otta-sh/payments-x402 |
x402 PaymentGateway adapter (synchronous page-gate, facilitator-verified). |
@otta-sh/plugin |
The EmDash plugin: commerce composition, storefront routes, admin console, content-sync hooks. |
@otta-sh/admin-presentation |
Pure admin presentation primitives (money, dates, short ids, status vocabulary) shared by both console surfaces. No IO. |
@otta-sh/admin-react |
The React admin console on the otta-console native descriptor (ADR-0014) — Orders and Pricing & inventory. |
sites/staging |
Staging storefront + admin — EmDash on Cloudflare Workers, plugin registered trusted. |
Design decisions live in adr/; development practices in
DEVELOPMENT.md; the agent-facing contract in CLAUDE.md.
pnpm workspace · tsdown builds · vitest tests · oxfmt (tabs) · oxlint (type-aware) · strict TypeScript.
pnpm lint # oxlint + domain-purity dependency check
pnpm typecheck # tsc -b
pnpm test # vitest (better-sqlite3 by default)
pnpm format # oxfmt, tabsThe concurrency tests are Postgres-required — better-sqlite3 serializes writes in one
process, so it verifies the SQL is correct, not that it's race-safe under contention. See
DEVELOPMENT.md for the TDD / contract-first workflow and commerce invariants.
v0.0.1 — first open-source release. The @otta-sh/* packages are all at 0.0.1 and are
not published to npm yet; consume them from the workspace.
The commerce layer is feature-complete (Phases 0–7 merged): catalog, inventory, cart, checkout, orders, customers with magic-link auth, Stripe + x402 payments, tax, shipping, discounts, entitlements, reporting, and settings.
The reference storefront (sites/staging) deliberately covers catalog + cart
only. The checkout / payment / download pages
(#27) and the customer account pages are
not built yet — so today you get a browsable catalog and carts with real inventory
holds, but completing a purchase end-to-end means building those pages or driving the
plugin's own commerce routes directly.
MIT
