Architecture

What an open-source headless storefront actually has to do

Rendering a product grid is the easy part. Here is the work that decides whether a headless storefront survives contact with a real shop.

Every framework has a commerce demo. Almost none of them are storefronts. The demo renders a catalogue, adds to a cart held in memory, and stops at the point where the work actually begins.

A storefront that can run a real shop has to do four things that a demo never does.

1. Hold cart state that survives everything

Cart is the hardest object in commerce, and it is hard for unglamorous reasons. A line item can go out of stock between the grid and the cart page. A coupon can be valid at add time and expired at checkout. A logged-out cart has to merge into a logged-in one without duplicating lines or silently dropping them. Quantity has to respect per-item stock limits the backend owns, not limits the frontend guesses.

None of that is visible in a screenshot, which is exactly why demos skip it and why it is the first thing to break in production.

2. Treat checkout as a state machine, not a form

Address, shipping method, payment method and order placement are sequential states with real failure modes at each transition. A shipping rate quote depends on the address. Payment methods depend on the region. An order that fails at the gateway must not leave the cart destroyed, and a successful order must not be re-placeable by a back button.

The reason this matters for a headless storefront specifically: every one of those transitions is a call into someone else's API, each with its own error vocabulary.

3. Refuse to fabricate

This is the one most projects get wrong, and it is a correctness issue rather than a completeness one.

When a platform has no endpoint for something the storefront wants, there are two options. You can return an empty list and let the UI render its empty state — honest. Or you can stub something plausible, which fails later as a 404 in somebody's live shop.

Authentication is where this becomes dangerous. A stub that mints a logged-in session for any password is not a placeholder, it is an authentication bypass. In this codebase, platforms with no real customer-auth endpoint throw rather than pretend. WooCommerce is the clearest example: POST /customers creates an account but returns no session, and core WordPress offers no storefront login without a JWT plugin, so auth throws instead of inventing one.

A related rule: never return an unscoped list where the storefront expects "mine". A GET /orders without a customer filter hands every order in the shop to whoever holds the key.

4. Keep the backend replaceable

A storefront that imports its platform's SDK throughout is portable in theory only. Every component naming a backend is a component to rewrite when the backend changes.

The fix is unglamorous: put the entire commerce surface behind one interface and let the platform-specific code live in a package you can swap.

// every page, on every backend
import { CartService, ProductService } from '$lib/core/services'

// kitcommerce.config.ts — the only file that names a platform
export * as services from '@misiki/vendure-connector'

That indirection is what turns "migrate to a different commerce platform" from a rewrite into a config change plus a package install.

What "open source" has to mean here

Checkout is where open-core models usually put the paywall, because it is the part you cannot avoid paying for. A storefront that is MIT except for checkout is not a storefront you own. Svelte Commerce is MIT throughout, checkout included.

The second half of owning it is being able to leave. Because the backend is one export, the exit cost from any single commerce platform is bounded — and because the code is MIT, the exit cost from the storefront itself is zero.

Try it against your own backend.