StorefrontsBuild your own frontend

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 fetch from 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 properties

An 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 createStorefrontClient only 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

  • createStorefrontClient is 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 to submit) per attempt.