Headless mode
Headless mode
Section titled “Headless mode”ecommus ships with a default storefront (apps/storefront-astro), but the framework is headless-first: the asset is the API + admin + super-admin. The Astro storefront is one of many possible frontends. You can disable it and render the public surface with whatever stack fits your team — Next.js, Nuxt, SvelteKit, native mobile, a kiosk, a marketplace bridge.
This page explains the contract for “going headless”, the trade-offs, and the minimal code to point a custom frontend at an ecommus API.
When to choose headless
Section titled “When to choose headless”| Scenario | Pick headless? |
|---|---|
| You already have a Next.js / Nuxt / Astro design system | ✅ |
| You want server components / partial hydration / streaming | ✅ |
| You need a native mobile storefront (Expo / Swift / Kotlin) | ✅ |
| You sell across web + a kiosk + a partner marketplace | ✅ |
| You want a single tenant with the default theme | ❌ (use Astro) |
| You’re early and don’t want to maintain a custom frontend yet | ❌ (use Astro) |
The default Astro storefront is production-grade — pick it unless you have a specific reason to roll your own.
What you keep when going headless
Section titled “What you keep when going headless”apps/api— the Fastify backend, plugin system, license gates, all middleware.apps/admin— your tenant operators still get the full admin UI (catalog, orders, customers, niche-package activation).apps/super-admin— platform / tenant provisioning, license panel, marketplace publisher.- All plugins + themes the API exposes via REST — payment, shipping, marketplace, e-Factura, niche packs.
- The license model + heartbeat + grace state — unchanged.
What you replace: apps/storefront-astro. Your custom frontend takes over the public-facing rendering.
Public surface contract
Section titled “Public surface contract”Headless integrators consume the storefront scope of the API (/api/storefront/*) — public routes, no auth required by default, tenant-resolved by host or X-Ecommus-Tenant header. The full surface is documented at API Reference (auto) and snapshot-tracked under contracts/public-surface.snapshot.json — see Trust commitment for the no-breaking-change guarantee within a major version.
The TypeScript SDK at @ecommus/client is the recommended entry point. It’s a typed thin wrapper over fetch with auto-injected tenant resolution + retry + telemetry hooks.
Architecture diagram
Section titled “Architecture diagram”┌──────────────────────────────────────────────────────────────────────────────┐│ Your custom frontend(s) ││ Next.js / Nuxt / SvelteKit / Expo / Swift / Kotlin / kiosk / partner ││ ││ Rendered with your design system. Deployed to your CDN. │└──────────────┬───────────────────────────────────────────────────────────────┘ │ @ecommus/client.storefront.* (or raw fetch) │┌──────────────▼───────────────────────────────────────────────────────────────┐│ apps/api (Fastify 5, hosted by you OR by us) ││ /api/storefront/* — public catalog, search, cart, checkout ││ /api/admin/* — JWT-protected, used by apps/admin ││ /api/super-admin/* — platform ops, used by apps/super-admin │└──────────────────────────────────────────────────────────────────────────────┘Admin + super-admin keep talking to the API as before — they don’t care that the public storefront is now custom.
Disabling the default storefront
Section titled “Disabling the default storefront”Headless deploys typically don’t ship apps/storefront-astro at all. Two ways to do it:
-
Build flag — pass
--filter=!apps/storefront-astrototurbo build:Terminal window npx turbo build --filter=!apps/storefront-astro -
Deployment-time — your CD pipeline simply doesn’t deploy the
apps/storefront-astroartifact. Caddy / nginx routes the public domain to your custom frontend instead.
You can keep the source in tree (it’s a useful reference + rendering test fixture). Nothing breaks.
Minimal Next.js consumer
Section titled “Minimal Next.js consumer”A barebones product page in a Next.js 15 / React 19 app:
import { ecommusClient } from "@ecommus/client";
const client = ecommusClient({ baseUrl: process.env.ECOMMUS_API_URL!, // https://api.mystore.ro tenant: process.env.ECOMMUS_TENANT_HOST!, // mystore.ro (or X-Ecommus-Tenant)});
export default async function ProductPage(props: { params: Promise<{ slug: string }>;}) { const { slug } = await props.params; const product = await client.storefront.products.bySlug(slug);
if (!product) return <div>Produs inexistent</div>;
return ( <article> <h1>{product.name}</h1> <p>{product.description}</p> <p> {(product.priceCents / 100).toFixed(2)} {product.currency} </p> </article> );}Add to cart and checkout flow:
import { ecommusClient } from "@ecommus/client";
const client = ecommusClient({ /* …same as above… */});
export async function POST(req: Request) { const { sku, qty, sessionId } = await req.json(); const cart = await client.storefront.cart.addItem({ sessionId, sku, qty }); return Response.json(cart);}Minimal Vue / Nuxt 3 consumer
Section titled “Minimal Vue / Nuxt 3 consumer”<script setup lang="ts">import { ecommusClient } from "@ecommus/client";
const client = ecommusClient({ baseUrl: useRuntimeConfig().public.ecommusApiUrl, tenant: useRuntimeConfig().public.ecommusTenantHost,});
const route = useRoute();const { data: product } = await useAsyncData( `product-${route.params.slug}`, () => client.storefront.products.bySlug(route.params.slug as string));</script>
<template> <article v-if="product"> <h1>{{ product.name }}</h1> <p>{{ product.description }}</p> <p>{{ (product.priceCents / 100).toFixed(2) }} {{ product.currency }}</p> </article></template>Tenant resolution
Section titled “Tenant resolution”The API resolves tenant in this order (see apps/api/src/middlewares/auth.ts):
X-Ecommus-Tenantheader — explicit, wins everywhere- Host header —
mystore.ro→ tenant whose domain ismystore.ro - Subdomain —
acme.platform.cloud→ tenantacme(multi-tenant SaaS) - Single-tenant fallback — if only one tenant exists in the DB, it’s used
For headless deploys, set X-Ecommus-Tenant explicitly in the SDK config. It avoids brittle host-based resolution when your frontend lives on a different domain than the API.
Auth for protected storefront calls
Section titled “Auth for protected storefront calls”Most storefront routes are public (catalog browse, search, view product). A subset requires a session token:
| Endpoint | Auth |
|---|---|
GET /api/storefront/products/* | none (public) |
POST /api/storefront/cart/* | session cookie or sessionId |
POST /api/storefront/checkout/* | session cookie or sessionId |
GET /api/storefront/orders/me | customer JWT (after login) |
Customer auth uses standard email/password → /api/storefront/auth/login returns an access JWT (15 min) + refresh token (30 d, rotating). Pass the access JWT as Authorization: Bearer <token> on protected calls. See API Authentication.
Plugin slots in headless mode
Section titled “Plugin slots in headless mode”Some plugins register slot fillers (e.g. niche-booking adds a date picker to product pages). In the default Astro storefront these render automatically. In headless mode, your frontend renders them.
Two patterns:
- Server-render the slot HTML — call
GET /api/storefront/slots/:slotName?productId=...to get the HTML string the plugin would emit, inline it in your page. Simplest, no JS coupling. - Read the slot manifest, render yourself — call
GET /api/storefront/slots-manifest?theme=<id>to get the slot definitions + default fillers as JSON, render with your own components. More work, full control.
Pattern 1 is recommended for niche-pack-style plugins where the customer doesn’t customise the rendering. Pattern 2 is for teams who want every UI atom to be theirs.
Trade-offs
Section titled “Trade-offs”| Aspect | Default Astro storefront | Headless |
|---|---|---|
| Time to first paint | Fast (Astro SSR + zero-JS by default) | Depends on your stack |
| Plugin slot rendering | Automatic | You wire it |
| SEO | Built-in | You own it |
| Theme manifest UI customization | Through themes/* packages | You build it |
| Updates from us | Free, every release | Storefront — yours; API + admin — free |
| Lock-in | Low (themes/* are open contracts) | Lower (you control the public surface) |
Headless trades convenience for control. The framework doesn’t punish you for choosing it — admin, super-admin, plugin slot-rendering APIs, OpenAPI spec, license model are all designed to support both modes equally.
Example apps (Day 2)
Section titled “Example apps (Day 2)”We’re building two reference repos:
examples/headless-nextjs/— Next.js 15 + React 19 + Tailwind, full storefront flow (browse, PDP, cart, checkout). Coming soon.examples/headless-nuxt/— Nuxt 3 + Vue 3, same scope. Coming soon.
Until those land, the snippets above + the auto-generated API reference are the canonical starting point. Open an issue if you want a different stack scaffold next (SvelteKit / Remix / Astro-as-consumer).
See also
Section titled “See also”- Architecture overview
- API Reference (auto)
- REST API Overview
- Authentication
- Trust commitment — public-surface no-breaking-change clause