Skip to content

Documentation

E-Commerce Overview

Turn a store on: the ecommerce preset, trader settings, gross prices and VAT, checkout, withdrawal, and the paid add-ons.

10/5/2026
18 min read Edit on GitHub

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:

  1. Pick the E-commerce preset in the Setup Wizard.
  2. Enable the Commerce plugin in Extensions.
  3. Fill the trader settings: legalName, legalAddress, legalEmail, phone, and either vatId or taxNumber. Checkout stays closed until those are set. The field list and the German button text are in European checkout.
  4. 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_rates row for every other country you ship to. “Choose later”, or a cleared standard rate, seeds the shipping zone and no tax row.
  5. Enable Stripe for cards, or leave checkout on cash on delivery / bank transfer.
  6. 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
ecommercecommercestripearchitecturepresetdashboardsmart-importer
Was this page helpful?