Migrate from a headless CMS
If your store is a custom frontend today — a Next.js app talking to a headless
CMS (Contentful, Sanity, Strapi) for content and to some commerce API or bespoke
backend for catalog and orders — this guide maps that architecture onto
Flightdeck. The good news is that the shape is already familiar: Flightdeck’s
public story is a headless one. You keep your frontend and your hosting; you
swap the commerce backend for @dscodotco/sdk and
adopt the store’s authored site manifest for merchant-controlled content.
Flightdeck never hosts your JavaScript. You own the frontend and its Vercel
(or other) deployment; the shopper’s browser talks to your server, and your
server talks to the commerce API through the SDK with a server-side
x-storefront-token. There is no cross-origin call and no CORS surface to
open — the same server-BFF shape you already run.
Two data planes, mapped
A headless-CMS storefront usually has two backends: a content plane (the CMS) and a commerce plane (products, cart, orders). Flightdeck consolidates the merchant-authored parts of both into one tenant.
| Your headless setup | Flightdeck | Notes |
|---|---|---|
| CMS product entries | Catalog products (store.products) | Product truth moves to the catalog; the SDK returns name, description, image_url, variants, price in cents. |
| CMS-managed collections / categories | Collections (store.collections) | Read-only over the SDK; membership is operator-managed. |
| CMS home/landing content, nav, theme | Site manifest (store.site.get()) | The merchant-authored sections, nav, and theme tokens — the store’s public content contract (ADR-0014). |
| CMS media library | Content-addressed assets | Product art resolves to image_url on the product; no separate lookup. |
| Your commerce API / cart service | store.checkout.submit | One-shot cart + card. See Checkout. |
| Your customer/session store | Shopper refs + store.shopper | Order history for a signed-in shopper; store credit balance. |
| Your order webhooks | orders.placed outbound webhook | See Webhooks. |
What does not move into the SDK. Catalog writes, collection management, and credential minting are not self-serve and are not part of the storefront SDK. Catalog is seeded with the scripted merchant-API loop (below); collections and going-live are operator-assisted. The storefront SDK is read + checkout. Plan the content model accordingly.
Decide what stays in your CMS
You do not have to give up your CMS. A clean split is:
- Move to Flightdeck: everything transactional or price-bearing — products, variants, prices, collections, checkout, orders, subscriptions. These must be authoritative on the commerce side so a tampered client cannot underpay.
- Keep in your CMS (optional): editorial content with no commerce truth — blog posts, long-form guides, marketing pages that do not carry a price.
For merchant-controlled storefront content — hero sections, nav, theme tokens — the native home is the site manifest, so you can retire that slice of the CMS if you want a single source of truth.
The site manifest replaces CMS-authored layout
store.site.get() returns the store’s active site manifest: the
merchant-authored sections, navigation, and theme tokens that make up the
storefront’s public contract. This is the piece that replaces “CMS-authored
homepage layout” in your old stack.
const manifest = await store.site.get();
// Render your nav, hero, and section order from the manifest;
// pull theme tokens (colors, type) from it too.Fail-closed: an unknown tenant or a store that is not live throws
FlightdeckError with status: 404 — you never receive an empty manifest.
Render from the manifest; do not paper a 404 over with a hard-coded default.
(A failed read is never an empty result — the platform-wide convention.)
Authoring the manifest itself is done through the site builder / merchant surface, not the storefront SDK. See Site builder for what the manifest can express.
Wire the SDK into your existing frontend
Because you already run a server-side data layer, adoption is mostly swapping the fetchers.
Install and construct the client server-side
npm install @dscodotco/sdk// lib/store.ts — imported only from server code.
import { createStorefrontClient } from "@dscodotco/sdk";
export const store = createStorefrontClient({
apiUrl: process.env.FLIGHTDECK_API_URL!, // https://api.ruo.pro
tenant: process.env.FLIGHTDECK_TENANT!, // your tenant ref
storefrontToken: process.env.FLIGHTDECK_STOREFRONT_TOKEN!, // opaque operator-provisioned secret, server-side only
});Never construct the client in browser code. The storefront token authorizes placing orders and would ship in the bundle. Build it in a Route Handler, a server component, or an edge function. See Build your own frontend.
Replace your product fetchers
Where you used to read product entries from the CMS, read the catalog. Note the
list and get shapes precisely — products.list() takes no arguments today
(the generated query type is empty), and every product carries image_url and
variants directly.
const { products } = await store.products.list(); // no query args today
const product = await store.products.get("bpc-157"); // by slug; 404 throwsReplace category pages with collections
const { collections } = await store.collections.list(); // no query args today
const collection = await store.collections.get("peptides"); // collection + its productsReplace site search
Search does take a typed query, unlike the list endpoints:
const results = await store.search({ q: "peptide", limit: 10 });Price filter drift. The generated search query type exposes
price_min_cents / price_max_cents, but there is known drift between the
typed param and the runtime API for price filtering. If a typed price filter
does not narrow results, fall back to the low-level client and pass the price
params as a raw query (see SDK reference).
q, collection, limit, and offset behave as typed.
Repoint checkout at your server
Your old cart/checkout service becomes a single server call. Submit cart + card together; read every outcome branch.
try {
const result = await store.checkout.submit(body, { idempotencyKey });
return confirm(result.order.id, result.card_charged_cents);
} catch (err) {
if (err instanceof FlightdeckError) return declined(err); // e.g. payment_declined
throw err; // a real failure — never a fake success
}Full body shape and the fraud/tender rules are in Checkout.
Seed the catalog
Your CMS content does not import automatically. Bring products across with the
same scripted, idempotent loop documented for any migration: create each product
(keyed on slug), then its variants, treating 409 slug_exists as “already
imported”. The full script, the image-attach flow, and the honest gaps (no bulk
importer, create-time-only status, operator-managed collections) are in
Import an existing catalog. If you are coming
specifically from Shopify, Migrate from Shopify
adds the Shopify-field mapping.
Content model checklist
Inventory your CMS content types
Sort each into: commerce truth (moves to the catalog), storefront layout (moves to the manifest), or pure editorial (stays in the CMS if you want).
Map product fields to the catalog shape
title to name, handle to slug, body to description, price to integer
price_cents, options to variant label. Media becomes a content-addressed
asset resolved as image_url.
Decide manifest vs CMS for layout
If you want one source of truth, author hero/nav/theme in the site manifest and retire that CMS slice. If you keep editorial in the CMS, render it alongside the manifest-driven storefront.
Wire order events
Point your fulfillment/analytics integrations at the orders.placed outbound
webhook instead of your old order hooks. See Webhooks.
Related
- Build a storefront from scratch — the full consumer build.
- SDK reference — every namespace and method.
- Site builder — what the site manifest expresses.
- Import an existing catalog — the scripted catalog seed.
- Migrate from Shopify — if you are coming from Shopify specifically.