Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

499 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Otta — an open-source commerce layer for EmDash

License: MIT Version

Open source (MIT), version 0.0.1. The WooCommerce-equivalent for EmDash, Cloudflare's TypeScript CMS.

The Otta storefront: a product listing with three sample products, each showing generated coil artwork, a title, a description, a price, and whether it is in stock — the first is sold out, its price struck through

The reference storefront running locally, with prices and stock served by the commerce service — this is what the quick start below gives you.

What this is

Otta turns an EmDash site into a store. It ships as three parts:

  1. Otta plugin — a sandbox-clean EmDash plugin: storefront routes, content-sync hooks, cart/checkout orchestration, an admin console (pricing & inventory, orders, reports, settings), and x402 gating for digital goods. Talks to the commerce service over HTTP only (network:request + allowedHosts). The CMS owns content; every commercial field lives in the commerce service and is edited in the admin console.
  2. Otta commerce service — a standalone Node/Hono + Postgres service that owns all money and stock truth: catalog, inventory, cart, checkout, orders, customers, payments, tax, shipping, discounts, entitlements, reporting, and webhooks.
  3. 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).

Quick start (local, ~2 minutes)

A full store on your laptop — no Cloudflare account, no deploy. The site's D1 content database and R2 media bucket are emulated locally by the Astro Cloudflare adapter; only the commerce Postgres is real.

pnpm install

# 1. Commerce database — any Postgres works; a throwaway container is fastest.
#    (Host port 55432, not 5432, so it can't collide with a local Postgres.)
docker run -d --name otta-pg \
  -e POSTGRES_USER=otta -e POSTGRES_PASSWORD=otta -e POSTGRES_DB=otta \
  -p 127.0.0.1:55432:5432 postgres:16

# 2. Commerce service — migrates itself forward on boot, then listens on :3000.
PG_CONNECTION_STRING=postgres://otta:otta@127.0.0.1:55432/otta \
  pnpm dlx tsx@4 packages/service/src/index.ts
# 3. Storefront + admin, in a second terminal.
COMMERCE_SERVICE_URL=http://127.0.0.1:3000 pnpm --filter @otta-sh/site-staging dev

Check the service with curl http://127.0.0.1:3000/health{"ok":true}. Then 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. Their prices and stock live in the commerce service, which the seed does not touch, so give them some:

# 4. Price, stock and activate the demo products (third terminal, or reuse the first).
#    It reads the products' real ids from the CMS (matching the seed's slugs),
#    then prices and activates each one in the commerce service.
SITE_URL=http://localhost:4321 COMMERCE_SERVICE_URL=http://127.0.0.1:3000 \
  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 against Postgres. 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.

Two things to know: the service is run through tsx rather than its built dist bin because the @otta-sh/* packages aren't published yet and their workspace export maps point at TypeScript sources (#44); and this storefront covers catalog + cart only — see Status.

To deploy this for free on Cloudflare Workers, follow DEPLOYMENT.md §3.

Why two parts

EmDash's plugin sandbox has no atomic write, compare-and-set, or transaction. Plugins get no direct database access — everything crosses a capability-scoped RPC bridge as JSON copies — and ctx.storage is an unconditional upsert whose declared unique indexes are silently downgraded. Any read-then-write spans two bridge calls and can interleave, so the sandbox can't express a guarded update, a uniqueness constraint, or a multi-document commit. Those are the ordinary building blocks of an order pipeline, so for now the transactional database sits off to the side, in the commerce service.

The gap is closing. emdash-cms/emdash#2169 (ours, currently a draft) adds ctx.storage.<collection>.updateIf(id, { where, set?, delta? }) — a guarded UPDATE … RETURNING run inside the sandbox. Two sibling primitives, not yet proposed upstream, cover the rest: an atomic insert that classifies unique violations, and ctx.storage.batch([...]) for all-or-nothing multi-collection writes. With all three, a @otta-sh/store-emdash adapter already passes the domain's full InventoryStore contract in-process. Once orders, payments, webhooks, and reporting follow, the split becomes a deployment choice rather than a correctness requirement.

Architecture (summary)

  • Product model = hybrid. Content (title, description, images, SEO, taxonomies) lives in a native EmDash products collection; commercial data (price, SKU, stock, tax, shipping) lives in the commerce service. Link key = the CMS content id.
  • Separate databases. Commerce Postgres is independent of the EmDash content DB (independent scaling; no cross-DB joins — joined in app code at render time).
  • Ports and adapters. @otta-sh/domain is pure (no IO); every store is a Kysely adapter dialect-parameterized over better-sqlite3 (dev) and Postgres (CI/prod). The REST API in @otta-sh/service mirrors the domain ports 1:1, and the same client-side contract suite runs over the wire so the HTTP format can't drift from the port.
  • Pluggable payments. Stripe (async webhook) and x402 (HTTP-402 at the page layer) behind one PaymentGateway interface.
  • Deployment. Runs on Cloudflare Workers via Hyperdrive over Neon Postgres, with cron sweeps for cart/reservation expiry. First-party sites may register the plugin trusted (in-process) to stay on the Workers free plan — 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.

Repository layout

Package What it is
@otta-sh/domain Pure ports, use-cases, branded money types, contract-test suites. No IO.
@otta-sh/service Thin Hono REST API + Cloudflare Worker entry mirroring the domain ports.
@otta-sh/store-postgres Kysely store adapters (better-sqlite3 local, pg CI/prod) + forward-only migrations.
@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: storefront routes, admin console, content-sync hooks.
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.

Development

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, tabs

The 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.

Status

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 service 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 service API directly.

License

MIT

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages