Build your own frontend
You don’t have to use the starter. Any framework works, as long as you follow
one rule: the storefront token is a server-side secret. The SDK is plain
fetch, so it runs anywhere — Next.js, Remix, SvelteKit, Astro, a bare Node
server, an edge function.
The BFF pattern
The shape is always the same:
shopper's browser ──► YOUR server (BFF) ──► @dscodotco/sdk ──► Flightdeck API
(holds the token)The browser talks only to your server. Your server holds
FLIGHTDECK_STOREFRONT_TOKEN and makes the SDK calls. Because it’s all
same-origin (browser -> your server), there is no CORS to configure, and the
token never ships in a bundle.
Browser calls fail by design. The API intentionally sends no CORS headers (ADR-0008): a
fetchfrom a shopper’s browser directly to the API origin will be blocked by the browser. That’s not a misconfiguration to work around — it’s the contract. Every call, reads included, goes through your server.
Reads (catalog, search) — server components or handlers
Catalog reads are safe to do in a server component or a Route Handler:
// A React Server Component, or an API route — server-side.
import { createStorefrontClient } from "@dscodotco/sdk";
const store = createStorefrontClient({
apiUrl: process.env.FLIGHTDECK_API_URL!,
tenant: process.env.FLIGHTDECK_TENANT!,
storefrontToken: process.env.FLIGHTDECK_STOREFRONT_TOKEN!,
});
export default async function Page() {
const { products } = await store.products.list({ limit: 24 });
return <Grid products={products} />;
}The authored site (sections, nav, theme)
The merchant authors their store — section layout, header nav, theme tokens —
in the console, and that content is part of the same public contract:
store.site.get() returns the live store’s active manifest
(GET /v1/tenants/{tenant}/site). Use it so your frontend reflects what the
merchant authored instead of re-declaring nav and palette in code:
const { manifest } = await store.site.get();
const nav = manifest.template?.sections?.find((s) => s.type === "header-nav");
const links = nav?.props.links ?? []; // [{ label, href }, …] — the authored menu
const theme = manifest.theme; // token tree -> your CSS custom propertiesAn unknown tenant or a non-live store throws a 404 (FlightdeckError) —
never an empty manifest. See Theming for the manifest
shape and token conventions.
Writes (checkout) — always a server handler
Card fields must never touch a third party from the browser. Post them to your own handler, which calls the SDK:
// app/api/checkout/route.ts (Next.js) — or your framework's equivalent
export async function POST(request: Request) {
const body = await request.json();
const result = await store.checkout.submit(
{
customer_ref: body.customerRef,
items: body.items,
card: body.card, // received over HTTPS, forwarded server-side
},
// REQUIRED — sent as the Idempotency-Key header. Deterministic per
// checkout attempt, so a retry replays instead of re-charging.
{ idempotencyKey: `cart-${body.cartId}:attempt-${body.attempt}` },
);
return Response.json(result);
}See Checkout for the full flow (idempotency, fraud, the authoritative total).
The one rule, restated
Construct
createStorefrontClientonly in server code. If you find yourself importing it into a"use client"component or a browser bundle, stop — the token would leak. Reads and writes both go through your server.
Client-side cart
The API has no server-side cart and no hosted checkout — checkout is one-shot.
So your cart lives client-side (local/cookie state) and you only call the API to
price (catalog reads) and to place the order (checkout). The starter’s
use-cart is a working example; the Next.js Commerce adapter implements the
same idea with a cookie-backed cart (see the adapter guide).
Types without the client
If you want the API’s types but not the client wrapper, the SDK re-exports the
generated paths / components / operations:
import type { components } from "@dscodotco/sdk";
type Product = components["schemas"]["…"];Checklist for a custom frontend
-
createStorefrontClientis imported only in server modules. - The storefront token is a server env var, never
NEXT_PUBLIC_. - Card fields are posted to your own handler, never to a third party from the browser.
- Reads distinguish 404 (real absence) from thrown errors (don’t render empty on failure).
- Checkout passes a stable, deterministic
idempotencyKey(the second argument tosubmit) per attempt.