The WooCommerce-equivalent commerce plugin for EmDash CMS — Cloudflare's Astro-native WordPress successor.
Typed end-to-end, sandbox-safe, edge-renderable. Every feature category Woo ships (products, cart, Stripe checkout, orders, shipping, tax, coupons, subscriptions, downloadables, reviews, multi-vendor) lands in a single plugin.
- 📘 Documentation: https://dashcommerce.dev/docs
- 💬 Issues: https://github.com/emdashCommerce/dashcommerce/issues
- 📦 npm:
@dashcommerce/core@0.1.2
v0.1.2 on npm. The v1.0 feature roadmap is code-complete (cart through Connect, hosted checkout, transactional email, starter theme). SemVer: 0.x may include minor breaking changes until 1.0.0 — see root CHANGELOG.md.
| Area | What ships |
|---|---|
| Products | Simple, variable (size/color/etc), grouped, external/affiliate, subscription, digital-download — one collection, one type switch |
| Multi-currency | Per-product price maps, customer-selected currency at cart, per-currency minor-units handling |
| Cart & Checkout | Hosted Stripe Checkout (default) + embedded Payment Element fallback; Apple/Google Pay; guest + logged-in |
| Orders | Admin dashboard with refund / partial-refund UI, order timeline, draft-to-paid pipeline |
| Customers | Address book, order history, self-service portal (email-link, no password needed for first access) |
| Coupons | Fixed/percent × cart/product, free-shipping, exclusions, usage limits, per-customer caps |
| Shipping | Multi-zone, flat-rate / free / local-pickup / weight-based; per-product shipping classes |
| Tax | Flat-rate, rate-table (by country/region), or Stripe Tax (automatic); tax on shipping toggle |
| Inventory | Soft-locks during checkout prevent overselling; low-stock alerts; backorder policy per product |
| Subscriptions | Stripe Subscriptions, trials, upgrade/downgrade, pause/resume, dunning, customer portal |
| Digital downloads | Signed-URL token downloads, TTL + max-use enforcement, per-order grants |
| Reviews | Moderation queue, verified-purchase badge, review aggregates on product pages |
| Multi-vendor | Stripe Connect Express onboarding, single-vendor-per-order splits, platform fee, vendor payouts |
| Abandoned cart | Cron-driven reminder emails with signed restore links |
| Transactional email | Receipt, refund, subscription renewal, dunning, abandoned cart, review request, vendor invite/activation/payout — HTML + plain text |
| Reports | Revenue / top products / top customers / MRR inside the admin dashboard |
| Admin UI | 12 React pages + 5 dashboard widgets + 2 field widgets + 3 Portable Text blocks |
| Package | Description |
|---|---|
@dashcommerce/core |
The plugin — hooks, routes, admin UI, storefront islands |
@dashcommerce/starter |
Reference EmDash storefront that exercises every feature |
The marketing / docs site source also lives in this monorepo under site/ and serves https://dashcommerce.dev.
bun add @dashcommerce/coreRegister the plugin in astro.config.mjs:
import { dashcommerce } from "@dashcommerce/core";
emdash({ plugins: [dashcommerce()] });Merge the products collection + taxonomies into your seed file and re-apply:
bunx dashcommerce-merge-seed
bun emdash seed --on-conflict=updateOpen /_emdash/admin/plugins/dashcommerce/settings and paste your Stripe test keys.
Want sample data to play with? Add --with-demo-catalog to seed six example products (one per type) plus curated category/tag terms:
bunx dashcommerce-merge-seed --with-demo-catalog
bun emdash seed --on-conflict=updateThe merge step is idempotent — it only replaces DashCommerce's own entries (the products collection, product_category / product_tag taxonomies) and, with --with-demo-catalog, only appends demo products whose ids aren't already in your seed. Everything else is preserved. If you prefer to assemble the seed in code, import mergeDashCommerceSeed(seed, { withDemoCatalog }) from @dashcommerce/core.
// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
import { local } from "emdash/storage/local";
import { dashcommerce } from "@dashcommerce/core";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({ directory: "./uploads", baseUrl: "/_emdash/api/media/file" }),
plugins: [dashcommerce()],
}),
],
});bun emdash init
bunx dashcommerce-merge-seed
bun emdash seed --on-conflict=update
bun devOr clone @dashcommerce/starter for a fully-wired storefront with demo catalog.
Full walkthrough: Getting started · Stripe setup.
astro.config.mjs
└─ emdash({ plugins: [dashcommerce()] })
│
├─ Vite build ──► packages/core/src/index.ts # descriptor only, side-effect-free
│
└─ Runtime ──► packages/core/src/sandbox-entry.ts # hooks + routes
│
├─ routes/cart.ts (public)
├─ routes/checkout.ts (public)
├─ routes/webhook.ts (public; Stripe-signed)
├─ routes/admin-api.ts (admin-gated)
└─ …12 more
Hard rules the sandbox entry and everything it imports obey:
- No Node built-ins (
fs,crypto,node:*) — crypto viacrypto.subtle, HTTP viactx.http.fetch - All money as integer minor units (
Money = { currency, amount }), ISO 4217; mixed-currency ops throw - Every Stripe webhook is idempotent via unique-indexed Stripe IDs; duplicates return HTTP 200
- Webhook signature verification happens before any side effect
- Cart re-prices server-side on every mutation and on every read — no client-sent prices trusted
bun install
bun run typecheck
bun run build
bun test # 66 tests across money / cart / coupons / webhook / tokens / splitPer-package loop:
cd packages/core && bun run dev # tsdown --watch
cd packages/starter && bun run dev # Astro on :4321Stripe webhook forwarding for local dev:
stripe listen --forward-to localhost:4321/_emdash/api/plugins/dashcommerce/checkout/webhookPre-release, so the surface is still shifting. Small PRs welcome; large new features please open an issue first so we can align with the roadmap in CHANGELOG.md.
MIT