For a future Claude session (or anyone) picking up where we stopped.
Original plan: /Users/timuzua/.claude/plans/glimmering-painting-fog.md — approved by the user and frozen except for scope creep from the user themselves.
Current repo: /Users/timuzua/Dev/dashcommerce (branch main).
| Phase | Commit | State |
|---|---|---|
| 1–5 | 098823f |
scaffold: types, money, storage, sandbox entry, cart routes |
| 6 | b5c93ed |
checkout + orders + refunds + inventory + cron |
| 7 | f2ae40e |
subscriptions + dunning + self-service |
| 8 | 56db64e |
digital downloads (signed token URLs) |
| 9 | 34436fe |
reviews + moderation + summary aggregation |
| 10 | d9361b9 |
Stripe Connect (Express onboarding + single-vendor split + payouts) |
| 11 | 6ccce80 |
abandoned-cart cron + restore token |
| 12 | todo | admin React UI (12 pages + 5 widgets) — see "Phase 12 reconnaissance" below |
| 13–19 | todo | storefront Astro, portable text, email templates, reports, install hook, tests, starter |
All commits typecheck clean (bun run typecheck) and build clean (bun run build from packages/core). No Node built-ins in the sandbox import graph.
DashCommerce — a WooCommerce-equivalent commerce plugin for EmDash CMS, built to ride the wave of EmDash's (Cloudflare's Astro-based WordPress successor) launch and ~10k GitHub stars in <1 month.
The user's direction was explicit: no MVP, no deferred tranche — ship everything WooCommerce has, in v1.0. Products (all 5 types including subscriptions + digital downloads), multi-currency from day one, cart, Stripe checkout, orders admin with refunds, customers, coupons, multi-zone shipping, Stripe Tax optional, inventory, reviews, subscriptions via Stripe Subscriptions, Stripe Connect multi-vendor, abandoned-cart recovery, transactional emails, React admin UI that later ports to Block Kit for marketplace distribution.
All typed, typechecks clean (bun run typecheck), builds clean (bun run build), sandbox-safe.
packages/core/src/
├── index.ts # native descriptor factory + re-exports
├── sandbox-entry.ts # definePlugin({ hooks, routes }) — wired to content hook + cart routes
├── storage-collections.ts # 19 plugin storage collections w/ indexes + uniqueIndexes
├── money.ts # Money type + arithmetic + format/parse; currency-safe; no floats
├── types.ts # full entity type surface (~470 LoC)
├── seed/products-collection.ts # defineProductsCollection() helper for host seed.json
├── hooks/content.ts # content:beforeSave product field validation
├── products/
│ ├── types.ts # purchasability + shipping predicates
│ ├── pricing.ts # resolvePrice({ product, variant?, currency })
│ └── variants.ts # variant CRUD against ctx.storage.product_variants
├── cart/
│ ├── store.ts # KV CRUD (cart:{sessionId})
│ └── calculate.ts # subtotal → discount → shipping → tax → total pipeline (pure)
├── coupons/validate.ts # validateCoupon + resolveDiscount (all 5 discount types)
├── shipping/calculate.ts # flat / free / pickup / weight-based rate options per zone
├── tax/calculate.ts # tax table engine (multi-rate, compound support)
├── stripe/
│ ├── client.ts # ctx.http.fetch wrapper; urlencoded; Idempotency-Key; Connect header
│ ├── payment-intents.ts # createPaymentIntent + retrievePaymentIntent
│ └── webhook-verify.ts # verifyStripeSignature via crypto.subtle (Web Crypto)
├── routes/cart.ts # public: GET /cart, POST /cart/items, /cart/currency, /cart/shipping-address
├── admin/entry.tsx # React admin entry stub — empty pages + widgets maps
└── astro/index.ts # blockComponents export stub
Infrastructure:
package.json(monorepo root + workspaces),tsconfig.base.json,biome.json,.gitignore,LICENSE(MIT),README.md,CHANGELOG.md.github/workflows/ci.yml— typecheck + build + testpackages/core/tsdown.config.ts— ESM build with.d.ts, neutral platform
The emdash install ships a creating-plugins skill at
/Users/timuzua/Dev/poc/new-wp-node/.agents/skills/creating-plugins/references/admin-ui.md
that is the authoritative source. It documents:
usePluginAPI()from@emdash-cms/admin— auto-prefixes plugin id on paths- primitives
Card,Button,Input,Select,Toggle,Table,Loading,Alertexported from@emdash-cms/admin pages+widgetsmaps as the admin entry exports- pages mount at
/_emdash/admin/plugins/<plugin-id>/<path>
The installed @emdash-cms/admin@0.4.0 in the POC lags the skill (those
symbols aren't yet in the compiled index.d.ts), but the skill is the
target API. Phase 12 is written against it using
src/admin/emdash-admin.d.ts — an ambient declaration that augments
@emdash-cms/admin with the skill's documented surface so this package
typechecks today and binds to the real exports on a newer host.
See .claude/skills/dashcommerce-continuation/SKILL.md for the orchestration skill, and the per-phase reference files in that directory for blueprints.
High-level sequence:
| Phase | Focus | Status |
|---|---|---|
| 6 | Checkout + orders + refunds + inventory + cron (the critical payment path) | ✅ b5c93ed |
| 7 | Subscriptions | ✅ f2ae40e |
| 8 | Digital downloads | ✅ 56db64e |
| 9 | Reviews | ✅ 34436fe |
| 10 | Stripe Connect | ✅ d9361b9 (single-vendor split; multi-vendor returns 501 with clear error) |
| 11 | Abandoned cart recovery | ✅ 6ccce80 |
| 17 | Install hook + onboarding DX | ✅ 05f902b (shipped early; unblocks cron scheduling + secret rotation) |
| 18 | Tests | ✅ d4dc83c (66 tests, 7 files — Money, cart, coupons, webhook, status, tokens, split) |
| 12 | Admin React UI | ✅ (current commit) — 12 pages, 5 widgets, admin-api.ts, ambient d.ts for @emdash-cms/admin |
| 16 | Reports | ✅ (rolled into phase 12 — admin-api.ts has revenue/top-products/top-customers/mrr) |
| 13 | Storefront Astro components + Stripe Elements islands | pending |
| 14 | Portable Text blocks | pending |
| 15 | Transactional email templates | pending (plain-text receipts already wired from phase 6/7/11) |
| 19 | Starter example + screenshots + npm publish | pending |
- No Node built-ins in
sandbox-entry.tsor anything it transitively imports. Crypto viacrypto.subtle. HTTP viactx.http.fetch. Norequire, nofs, nopath, nonode:*. This keeps the port to standard format mechanical. - All money as
Money(integer minor units + ISO 4217 currency). Arithmetic viamoney.tshelpers only. Mixed-currency ops throwCurrencyMismatchError. - Idempotency on every Stripe webhook via unique-index dedup on
stripePaymentIntentId,stripeRefundId,stripeInvoiceId,stripePayoutId. Always return HTTP 200 on duplicates. - No transactions across collections. Use
putManyfor atomic batches (e.g. order + order_items). Sequence multi-collection writes so partial failure is recoverable and cron can re-drive. - Webhook signature verification MUST happen before any side effect. 400 on fail. See
stripe/webhook-verify.ts. - Cart must be re-priced server-side at checkout. Never trust client-sent prices. Re-resolve using
products/pricing.tsin cart currency against current product/variant data. - ISO 8601 timestamps for all dates — string-compare for index ordering.
- Products live on the host content collection (
products), declared viadefineProductsCollection()in seed.json. Plugins cannot declare content collections. Commerce-specific data lives on the content item'sdataobject, validated inhooks/content.ts. - Variants, orders, customers, coupons, shipping, subscriptions, reviews, vendors, inventory_ledger, download_grants → plugin storage. 19 collections declared in
storage-collections.ts. - Carts + stock soft-locks → KV (prefixes
cart:andlock:). Cron sweeps expired locks every 5 min. - Native plugin format now, Block Kit port later. Current descriptor has
format: "native",adminEntry,componentsEntry.definePlugin({})in sandbox-entry is the standard shape (no id/version); emdash readsformatfrom the descriptor. - Low-stock alerts fire once per product per below-threshold crossing. Track
belowThresholdAton product fields; reset on restock.
cd /Users/timuzua/Dev/dashcommerce
bun install
bun run typecheck # tsc --noEmit across workspaces
bun run build # tsdown for core
bun test # phase 18+ onlyFrom packages/core specifically:
cd packages/core
bun run typecheck
bun run build
bun run dev # tsdown --watchOnce phase 19's starter exists, the full loop is:
cd packages/starter
bun install
bun emdash seed
bun dev # Astro at :4321
stripe listen --forward-to localhost:4321/_emdash/api/plugins/dashcommerce/checkout/webhookReference emdash install at /Users/timuzua/Dev/poc/new-wp-node (the user's POC site — has emdash@0.4.0 + @emdash-cms/plugin-audit-log — we use these node_modules as our type + pattern reference).
| What | Where | Notes |
|---|---|---|
PluginDescriptor interface |
node_modules/emdash/dist/index-CRg3PWfZ.d.mts lines 2175–2231 |
Descriptor shape — format, adminEntry, adminPages, adminWidgets, capabilities, allowedHosts, storage, componentsEntry |
StorageCollectionDeclaration (descriptor) |
Same file lines 2178–2181 | Only indexes: string[] + uniqueIndexes?: string[] — no composite tuples at descriptor level |
StorageCollectionConfig (runtime) |
node_modules/emdash/dist/types-BYWYxLcp.d.mts lines 112–124 |
Array<string | string[]> — composites allowed at runtime |
WhereValue, RangeFilter, InFilter, StartsWithFilter |
Same file lines 132–147 | Legal where clause values |
StorageCollection<T> API |
Same file, search for interface StorageCollection |
get/put/query/count/putMany/deleteMany; query returns { items: [{id, data}], cursor, hasMore } |
KVAccess, PluginContext, hook types |
Same file | Used throughout |
| Audit-log plugin (standard-format reference) | node_modules/@emdash-cms/plugin-audit-log/dist/sandbox-entry.mjs |
Read patterns for hooks + routes |
| WooCommerce source (feature-map reference) | /Users/timuzua/Dev/poc/new-wp-node/woocommerce/ |
PHP source — useful for scoping edges on coupons, order states, shipping |
- PluginDescriptor uses
adminEntry, notadmin.entry. The creating-plugins skill docs show a nestedadmin: {entry, pages, widgets}shape — that's wrong for the descriptor. Flat fields:adminEntry,adminPages,adminWidgets,componentsEntry. - Composite indexes don't compile at descriptor level.
PluginDescriptor.storage[*].indexesisstring[]only. If you need composite queries, handle them at the app layer with compoundwhere. whereclauses don't acceptRecord<string, unknown>. Values must bestring | number | boolean | null | RangeFilter | InFilter | StartsWithFilter. Type the where object accordingly.- Stripe webhook verify must use Web Crypto, not Node's
crypto.createHmac. If you seeimport { createHmac } from "crypto"anywhere insandbox-entry.ts's import graph, stop and fix it — it will break in the sandbox port. definePlugin({})with no id/version returnsStandardPluginDefinition. That's fine for the hybrid path — the descriptor'sformat: "native"+entrypoint: "@dashcommerce/core/sandbox"tells emdash how to load it.- tsdown emits
.js+.d.ts, not.mjs+.d.mts, when package.json hastype: "module". Our exports match this.
- Open this file.
cd /Users/timuzua/Dev/dashcommerce && git log --oneline -5to confirm the last commit.- Run
bun install && bun run typecheck && bun run buildto confirm the tree still compiles. - Load the continuation skill:
/skill dashcommerce-continuation(if available), OR read.claude/skills/dashcommerce-continuation/SKILL.mdmanually. - Pick the next unchecked phase from the table above.
- Read the matching reference file in
.claude/skills/dashcommerce-continuation/references/. - Write code. Typecheck after each file. Build before committing.
- One commit per phase, in the format:
feat: phase N — <what>.
The user's tolerance for partial work is low — they want things that run, not stubs. Prefer completing fewer phases well over scaffolding many badly.