GuidesMigrateFrom a headless CMS

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 setupFlightdeckNotes
CMS product entriesCatalog products (store.products)Product truth moves to the catalog; the SDK returns name, description, image_url, variants, price in cents.
CMS-managed collections / categoriesCollections (store.collections)Read-only over the SDK; membership is operator-managed.
CMS home/landing content, nav, themeSite manifest (store.site.get())The merchant-authored sections, nav, and theme tokens — the store’s public content contract (ADR-0014).
CMS media libraryContent-addressed assetsProduct art resolves to image_url on the product; no separate lookup.
Your commerce API / cart servicestore.checkout.submitOne-shot cart + card. See Checkout.
Your customer/session storeShopper refs + store.shopperOrder history for a signed-in shopper; store credit balance.
Your order webhooksorders.placed outbound webhookSee 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 throws

Replace category pages with collections

const { collections } = await store.collections.list();       // no query args today
const collection = await store.collections.get("peptides");   // collection + its products

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.