E-Commerce Overview
Turn a store on: the ecommerce preset, trader settings, gross prices and VAT, checkout, withdrawal, and the paid add-ons.
On this page
This is the operator task: decide what to switch on, configure the catalog and the providers, and run orders. How the engine is built — extension surfaces, the totals pipeline, the checkout services, the HTTP contract — is E-commerce architecture.
SveltyCMS is a headless CMS first. Most installs never run a store. Commerce is therefore opt-in, not part of the boot path:
| Layer | What it is | When it loads |
|---|---|---|
| Core CMS | Collections, widgets (including generic Price/Currency), RBAC, tenants | Always |
| Ecommerce preset | Optional Setup Wizard template: products, carts, orders, coupons, tax, shipping | Only if you pick E-commerce |
| Smart Importer | WooCommerce (WXR/REST) and Shopify catalog → products (+ variants) |
Only when the plugin is enabled |
| Dashboard widgets | Commerce Orders + Commerce Inventory (freemium, 14-day trial) | Add from the dashboard picker; empty state if the preset is missing |
| Commerce plugin | Cart, gross-price VAT, inventory, guest /api/commerce/* checkout, withdrawal |
Only when the plugin is enabled |
| Stripe plugin | Payment adapter: keys, Elements, PaymentIntent, webhooks | Only when enabled — also usable for memberships / gated content without Commerce |
To run a store:
- Pick the E-commerce preset in the Setup Wizard.
- Enable the Commerce plugin in Extensions.
- Fill the trader settings:
legalName,legalAddress,legalEmail,phone, and eithervatIdortaxNumber. Checkout stays closed until those are set. The field list and the German button text are in European checkout. - Keep or edit the tax row the wizard seeded for the country on the form. The form starts on Germany (19% and 7%, shipping taxable). Add a
tax_ratesrow for every other country you ship to. “Choose later”, or a cleared standard rate, seeds the shipping zone and no tax row. - Enable Stripe for cards, or leave checkout on cash on delivery / bank transfer.
- Add the Commerce Orders and Commerce Inventory dashboard widgets if you want them on the admin home.
The preset’s tax_rates and shipping_zones are enough for a quote. TaxJar and live shipping are separate paid plugins. In this tree they apply a sandbox rate from plugin settings. They do not call TaxJar or a carrier over HTTP.
What loads on a blog/SaaS install (Commerce off, no ecommerce preset): kernel + whatever field widgets the content model uses. No carts collection, no /shop, no Stripe SDK, no order dashboard widgets unless someone adds them (they render empty).
Stripe is a gateway, not the store. Shipping, tax, coupons, and carts belong to the Commerce plugin, so a different PSP can replace Stripe without touching the catalog or checkout.
Status (2026-10-05): ecommerce preset, gross catalog prices with VAT taken out of that total, guest checkout on /api/commerce/*, and the storefront /shop, /shop/{slug}, /cart, /checkout, /widerruf, /legal/impressum, /legal/privacy, /legal/terms, /account/orders, /account/addresses. Stripe charges the server grandTotal. Smart Importer and the two freemium dashboard widgets are in this tree. Commerce Pro license-gates gift cards, bundle lines, custom checkout panes, and store analytics.
Guest checkout runs when the Commerce plugin is enabled, the ecommerce collections exist, and the trader settings are filled.
What is in the product today
Ecommerce preset
Enable E-commerce in the Setup Wizard. Collections (verified db_fieldName values in src/routes/setup/presets.ts):
| Collection | Role |
|---|---|
products |
Catalog: gross price, taxClass (standard / reduced / zero), priceHistory (amount, recordedAt), SKU, inventory, variants (sku, title, price, inventory, downloadable, taxClass), categories, media |
product_categories |
Nested categories (parentCategory → product_categories) |
product_reviews |
Rating + visibility (approved boolean); product → products |
coupons |
percentage, fixed_cart, fixed_product, free_shipping; server-managed usedCount; expiresAt is a date |
tax_rates |
country, state, rate, reducedRate, label, shippingTaxable (default true) |
shipping_zones |
name, countries, method, rate, freeThreshold |
carts |
sessionId, customer (text), items, subtotal, coupon, expiresAt |
orders |
Line items, totals (total/totalCents/netTotal/taxRate/currency), invoiceNumber, addresses, status (see below), paymentMethod, stripePaymentIntentId, trackingUrl, deliveredAt, withdrawalAt, server-managed inventoryCommitted + cartId |
Headless storefronts read the catalog through /api/collections/products (auth and publication policy apply). Guest checkout runs on /api/commerce/* and the storefront pages /shop, /shop/{slug}, /cart, /checkout, /widerruf, and /legal/impressum, /legal/privacy, /legal/terms. /shop lists products whose status is publish. A product with status draft or unpublish cannot be added to the cart. A product with no status stays purchasable.
The wizard writes one Worldwide shipping zone (flat rate 5, blank countries so it matches every country) when that collection is empty. It also writes one tax row for the country on the market step when a standard rate is filled. The form starts on Germany (country DE, rate 19, reducedRate 7, label MwSt when the shop language is German and VAT otherwise, shippingTaxable true). “Choose later”, or a cleared standard rate, writes the shipping zone and no tax row. A seed call with no market still writes the German 19% and 7% row labeled VAT. Catalog prices are gross plain numbers (19.99 is the amount the buyer pays, including VAT). The Price widget stores { amount, currency }, which checkout reads as 0. A database seeded before 2026-10-05 keeps its previous tax row until you set shippingTaxable and reducedRate on it.
Catalog import — Smart Importer (shipped)
The Smart Importer plugin maps an existing store catalog into the ecommerce preset. It does not run checkout.
| Source | Format | What lands |
|---|---|---|
| WooCommerce | WXR XML or REST JSON (--format=woocommerce) |
products with nested product_variation, SKU, price, sale price, stock |
| Shopify | Products JSON / CSV (--format=shopify) |
Same ecommerce block (SKU, price, inventory, variants) |
Enable the plugin (Admin → Config → Migration tile), then import into products. Mixed WordPress dumps (posts + products) still use --format=wordpress; product items also get an ecommerce block from _sku / _price / _stock meta. Platform guide: WordPress migration (WooCommerce section). Editorial path: Migration & Data Operations.
After import, add the Commerce Orders / Inventory dashboard widgets to watch stock and recent orders (empty until you have those collections and data).
Admin dashboard widgets (shipped)
Two marketplace-portable packages under src/routes/(app)/dashboard/widgets/. Freemium — 14-day key-less trial, then checkExtensionLicense("dashboard", id). Not a cart widget.
| Widget | Marketplace id | Endpoint | Reads |
|---|---|---|---|
| Commerce Orders | dashboard:commerce-orders · €12.99 |
GET /api/dashboard/commerce-orders |
orders |
| Commerce Inventory | dashboard:commerce-inventory · €12.99 |
GET /api/dashboard/commerce-inventory |
products (variant-aware inventory) |
Add them from the dashboard widget picker. Missing preset collections → empty state, not 404. After trial without a key: <UpgradePrompt> on the client and 403 LICENSE_REQUIRED on the API. Details: Dashboard widgets, Dashboard API.
Order status
Preset options (do not invent paid / fulfilled):
pending · processing · shipped · delivered · cancelled · refunded
Mapping when integrating gateways: paid → processing, fulfilled → shipped.
Which transitions the server accepts, and what cancelling or refunding does to stock and coupon usage, is E-commerce architecture → Order state machine.
Stripe plugin (payments only)
Stripe stays a payment plugin. It must not grow into a store: no shipping tables, no tax engine, no cart.
| Shipped | Stripe’s job | Not Stripe’s job |
|---|---|---|
| Tenant-aware keys, Elements, webhook signature + upsert | Create/confirm PaymentIntent; map succeeded / failed / later refunded |
Cart, coupons, tax, shipping, inventory, order state machine |
| Payment status column on entries | Accept grandTotal from Commerce or a simple amount for gated content |
Trust client amount as the charge |
When Commerce is enabled, Stripe receives server grandTotal. When Commerce is off, Stripe can still charge a single amount (membership, donation, paywalled entry). Plugin docs: stripe.mdx. The PaymentGateway port, the amount check on confirm, and webhook scoping are E-commerce architecture → Payments and fulfillment ports.
Field widgets
Price and Currency store money on entries — including non-store money fields. Enter prices as decimal amounts (19.99) in the store currency (the Commerce plugin defaults to EUR); the engine converts them to integer minor units internally. Admin dashboard widgets (Orders / Inventory) are a separate package type — see above.
Like-for-like notes (SveltyCMS as of 2026-10-05)
Not a ranking. Like-for-like means a self-hosted CMS that can run a store. Shopify is a packaging reference (SaaS, a different product class). The SveltyCMS column matches this tree on 2026-10-05. The other columns are from public documentation reviewed in August 2026 and were not re-checked on 2026-10-05. Sources: Payload ecommerce overview and RFC #14239; Drupal Commerce 2.x developer guide; WooCommerce.com feature list. Re-check those pages before a purchase decision.
| Capability | SveltyCMS (this tree) | Payload ecommerce plugin | Drupal Commerce (modules) | WooCommerce (WordPress) |
|---|---|---|---|---|
| Catalog schema (products, variants) | Shipped (optional preset) | Documented native | Documented native | Documented native |
| Reviews | Shipped (product_reviews) |
Not listed as native | Contrib / custom | Native comments / plugins |
| Coupon / tax / shipping data | Shipped (collections; checkout quotes apply them) | Documented as native gaps / RFC | Native tax + promotions; shipping often contrib | Native + extensions |
| Catalog import (Woo / Shopify) | Shipped (Smart Importer; Woo/Shopify are Pro platforms) | Import paths vary by project | Migrate modules | N/A (it is the source) |
| Admin orders / inventory UI | Shipped (2 freemium dashboard widgets, 14-day trial) | Documented as part of the plugin admin | Native order / product UIs | Native wp-admin |
| Cart session, merge, guest checkout | Shipped (src/plugins/commerce/, opt-in) |
Documented native carts | Native commerce_cart |
Native |
| Totals engine (tax/ship/coupon applied) | Shipped (gross VAT extracted by default; computeTotals + quotes.ts) |
Documented native checkout; shipping/tax not native; Stripe adapter uses cart.subtotal (open issue) |
Adjustments + order processors | Native cart totals |
| Inventory reserve / decrement | Shipped (per-variant decrement/restore, orderRef) |
Inventory fields documented; multi-warehouse not first-class | Stock + order workflow | Native stock |
| Payments (Stripe) | Shipped (Elements, webhooks, server grandTotal intent) |
Stripe adapter documented | commerce_payment gateways |
Stripe / other gateways as extensions |
| Subscriptions / gift cards / bundles | Gift cards and bundles are Pro-gated. subscribe returns 501 |
Documented native gaps / RFC | Contrib | Often paid extensions (e.g. Subscriptions) |
| Multi-tenancy | Shipped (locals.tenantId on every commerce query) |
Not documented as built-in | Typically one site / Domain Access | Typically one site / multisite |
| Public storefront | Shipped (/shop, /shop/{slug}, /cart, /checkout, /widerruf, /legal/*, /account/orders, /account/addresses) |
Template documented | Theme / storefront | Theme |
A guest can complete a priced checkout when Commerce and the ecommerce preset are enabled and the trader settings are filled. Account order history, addresses, withdrawal, and transactional mail are in this tree. This tree has no promotion-group engine, no BOGO rules, no Stripe Billing subscriptions, and no Schema.org Product/Offer markup on /shop. Like-for-like notes only — not a ranking.
Not shipped yet {#still-open-not-a-separate-plan-file}
| Item | Notes |
|---|---|
ConditionPlugin / AND-OR promotion groups |
Not shipped |
| BOGO / tiered promotions, multi-warehouse, FX resolver | Commerce Pro backlog |
| Stripe Billing (real subscriptions) | subscribe checks the Commerce Pro license, then returns 501 |
computeTotals < 5 ms benchmark |
No commerce row in the matrix yet |
| HTTP integration: cart → order → refund, two tenants | Unit isolation exists; no integration file |
| Schema.org Product/Offer/Review, order emails already exist | Structured data on /shop not shipped |
The open rows above are tracked on the 2026 roadmap. The storefront steps that are in this tree are European checkout.
Commerce plugin (shipped, disabled by default)
Home: src/plugins/commerce/ (definePlugin, disabled by default). Enable it in Extensions — core CMS does not register checkout unless the plugin is on. Guest HTTP is /api/commerce/* — not /api/plugins/commerce (plugins:execute is never required).
With the plugin on and the ecommerce preset in place, a guest can browse /shop and /shop/{slug}, add to /cart, quote shipping and VAT on /checkout, and place an order. Signed-in customers get /account/orders and /account/addresses. Guests withdraw at /widerruf. Transactional mail (order received, withdrawal received, shipped, refunded, merchant new-order, low stock) goes out through the CMS email system. The order stays placed when mail fails.
A missing preset fails closed: 503 COMMERCE_PRESET_MISSING while products or carts is absent, 403 COMMERCE_DISABLED while the plugin is off. The module map, the HTTP actions, and the order state machine are E-commerce architecture.
European checkout {#european-checkout}
Operator steps for a German or EU consumer shop, as of 2026-10-05. This describes what the storefront records and shows. OSS returns, packaging registration (VerpackG / LUCID), and WEEE or battery take-back stay with the merchant.
Fill the Commerce public settings before the first order: legalName, legalAddress, legalEmail, phone, and either vatId or taxNumber. storeLanguage defaults to de, pricesIncludeTax to true, homeCountry to DE, and currency to EUR. Until the trader fields are complete, the order button stays disabled and checkout returns 409 TRADER_IDENTITY or 409 TRADER_TAX_ID.
/shop and /shop/{slug} show the gross amount, the currency, and “inkl. MwSt.” (“incl. VAT” when storeLanguage starts with en). The add-to-cart control stays Add to cart. A Sale badge appears when the current price is below the lowest priceHistory amount from the previous 30 days. The Commerce plugin appends the previous price when price changes and the plugin is enabled. An empty history leaves the badge off, including when only comparePrice is higher.
Checkout shows the trader, the quote (subtotal, shipping, net, VAT, total), and the withdrawal text before the order is created. The button is zahlungspflichtig bestellen in German and Order with obligation to pay in English. That submit creates the order. Stripe then takes the card for the server grandTotal.
Physical goods need a name, street, postal code, city, and country. The buyer accepts the terms and the withdrawal policy. A cart of only downloadable lines also needs the digital-performance consent (DIGITAL_CONSENT_REQUIRED). A destination country with no tax_rates row fails with TAX_RATE_MISSING.
VAT is taken out of the gross amount, once per rate, after the coupon and after taxable shipping: round(gross × rate / (100 + rate)). The charged total stays the gross total. reduced uses reducedRate when that column is set, otherwise the standard rate. zero is 0%. Leaving the wizard on Germany seeds 19% and 7%, and shipping on that row is taxable.
A buyer VAT ID is checked with the European Commission VIES REST service when the order is placed. The on-page quote stays the consumer total. Reverse charge (VAT amount 0, plus a note on the order) applies when VIES confirms the ID and the destination is another EU country. A format-invalid ID returns VAT_ID_INVALID. When VIES cannot be reached, the order keeps the VAT and stores a note that the ID was not verified. A VAT ID for the shop’s own country keeps the VAT.
When the TaxJar plugin is licensed and ready, its sandbox testRatePercent is added on top of the base and replaces this extraction. The sandbox uses that setting.
The order stores invoiceNumber (RE-{year}-{base36 time}), netTotal, taxTotal, taxRate, the address, and vatNote. The confirmation screen and the order email show net, VAT, and the total. Signed-in customers see the same figures on the receipt page.
/widerruf works without an account. The page is opened as Vertrag widerrufen (English: Withdraw from the contract). The submit button is Widerruf bestätigen (Confirm withdrawal). The order number and the customer email must both match. A pending or processing order is cancelled, stock is restored, and one coupon use is returned. A shipped or delivered order keeps its status and records withdrawalAt for a later staff refund. The period is 14 days after deliveredAt when the buyer acknowledged the withdrawal policy, and 12 months plus 14 days when that acknowledgement is missing. A downloadable-only order with the performance consent is closed. A second confirmation returns 409 WITHDRAWAL_CLOSED. The separate 60-minute cancel for a signed-in customer on a pending order is still there.
/legal/impressum, /legal/privacy, and /legal/terms render from the same Commerce settings.
Free vs paid {#monetization-marketplace-same-as-widgetsplugins}
Commerce is not a paid cart widget. It uses the existing license manager and the three marketplace tiers already documented for widgets: Free, Freemium (14-day key-less trial), Paid. Core checkout keeps working when a licence lapses — Pro features stop, paid add-ons fall back to the free tables.
What to sell vs what stays free
Give away a working store so people adopt the CMS; charge for extras that other ecosystems often sell as paid add-ons (gift cards, bundles, sandbox carrier rates — verify current third-party pricing before any public comparison).
| SKU | Marketplace id | Tier | Includes | After trial / without key |
|---|---|---|---|---|
| Ecommerce preset | — (Setup Wizard) | Free | Collections only | Always available if the template is chosen |
| Price / Currency widgets | core widgets | Free | Money fields | Never gated |
| Smart Importer (catalog) | plugin:smart-importer |
Freemium (already) | WooCommerce WXR/REST + Shopify JSON/CSV → products |
5 platforms free; extra store platforms (Shopify, Woo, …) are Pro |
| Commerce (plugin) | plugin:commerce |
Freemium | Cart, computeTotals, tax, shipping quotes, 4 coupon types, per-variant inventory, guest /api/commerce/*, order/refund machine, Stripe grandTotal handoff |
Basic checkout keeps working; Pro features strip / 403 LICENSE_REQUIRED |
| Commerce Pro (same plugin, premium fields) | plugin:commerce |
Paid add-on inside Freemium | Gift cards, bundle lines on add-to-cart, custom checkout panes, store analytics. subscribe checks the license and returns 501. BOGO, tiered promotions, multi-location inventory, and a currency resolver are not in this tree |
Without a key those calls return 403 LICENSE_REQUIRED (subscribe still 501 after the license check) |
| Stripe | plugin:stripe |
Freemium (already) | One-time PaymentIntent + webhooks | Subscriptions / invoices / analytics already stripped in beforeSave |
| Commerce Orders (dashboard widget) | dashboard:commerce-orders |
Freemium · €12.99 | Recent orders + status mix from the orders collection |
UpgradePrompt; GET /api/dashboard/commerce-orders → 403 LICENSE_REQUIRED |
| Commerce Inventory (dashboard widget) | dashboard:commerce-inventory |
Freemium · €12.99 | Low-stock / out-of-stock, variant-aware inventory |
UpgradePrompt; GET /api/dashboard/commerce-inventory → 403 LICENSE_REQUIRED |
| Live Shipping (UPS / FedEx / DHL) | plugin:shipping-live |
Paid · ~€24.99 | Sandbox testRateCents via ShippingRateProvider, labeled UPS, FedEx, or DHL. This tree does not call carrier HTTP |
Without a key, checkout keeps shipping_zones. A key applies the sandbox cents |
| TaxJar | plugin:taxjar |
Paid · ~€19.99 | Sandbox testRatePercent via TaxProvider, added on top of the base. This tree does not call TaxJar HTTP. A ready TaxJar quote replaces the included-VAT extraction |
Without a key, checkout keeps tax_rates. A key applies the sandbox percent |
| Future PayPal (etc.) | plugin:paypal |
Paid or Freemium | Another PaymentGateway |
Independent license |
Do not license-gate: enabling the Commerce plugin, adding to cart, coupon validate, tax/shipping quote, or placing an order with the four preset coupon types. That would make “CMS that is not always a store” painful and would hide the free core. Do license-gate admin extras: the Commerce Orders and Commerce Inventory dashboard widgets (14-day key-less trial, then checkExtensionLicense("dashboard", id)). They never load a store into a blog install — if the ecommerce preset collections are missing, the widgets render an empty state.
Dashboard widget SKUs (dashboard:commerce-orders, dashboard:commerce-inventory) use the same 14-day trial as dashboard marketplace. Listing them on marketplace.sveltycms.com is what turns verify-404 (local/free) into a paid SKU after the trial. See Admin dashboard widgets above.
Indicative list prices: Commerce Pro ~€79 / year (or a lifetime SKU on the marketplace); Stripe plugin ~€4.99; dashboard widgets €12.99. Stripe stays its own SKU so a membership site can pay for Stripe Pro without buying a store. List the packages on marketplace.sveltycms.com or verify-404 treats them as local/free.
Related
| Doc | Why |
|---|---|
| E-commerce architecture | Surfaces, totals pipeline, checkout services, HTTP, tests |
| Roadmap 2026 | Remaining commerce backlog |
| Competitive comparison | Commerce packaging table |
| Setup Wizard | Ecommerce preset |
| Site Starter | Public /shop, /cart, /checkout, /account |
| Smart Importer | Shopify JSON + WooCommerce WXR/REST catalog → products |
| WordPress / WooCommerce migration | WXR + --format=woocommerce catalog path |
| Migration & Data Operations | Admin Migration tile |
| Dashboard widgets | Commerce Orders + Inventory packages, 14-day trial |
| Dashboard API | /api/dashboard/commerce-orders and commerce-inventory |
| Widget marketplace licensing | Free / Freemium / Paid + checkExtensionLicense |
| Marketplace | Catalog, plugin:commerce listing, license verify |
| Live Shipping | Paid UPS / FedEx / DHL plugin |
| TaxJar | Paid live tax plugin |
| Email system | SMTP for order mail |