Astro (SSR)

Astro renders on the server by default: .astro page frontmatter runs on your server, not in the browser. With an SSR adapter enabled, that frontmatter and Astro endpoints become your BFF — they hold the storefront token and talk to the commerce API. This guide builds a catalog, a PDP, and a checkout endpoint.

Why Astro needs SSR here

The storefront token — an opaque secret (no fixed prefix), provisioned by the platform operator; platform-wide today — is server-side only, and the API sends no CORS headers, so the browser cannot call it directly. Astro’s default static output would run your fetches at build time and bake the results (and risk the token) into shipped files. You want on-demand rendering: install an SSR adapter so page frontmatter and endpoints execute per request on your server.

browser  ->  Astro page frontmatter / endpoint  ->  @dscodotco/sdk  ->  commerce API
                    (holds the storefront token)
⚠️

The token must never appear in a client:* component or in anything Astro ships to the browser. Read it only in .astro frontmatter and in src/pages/**/*.ts endpoints. Client components receive already-fetched, token-free data as props.

Before you start

  • Node 18+.
  • A tenant ref, a storefront token (an opaque secret, no fixed prefix, provisioned by the platform operator; platform-wide today), and the API origin https://api.ruo.pro.

Create the app and enable SSR

npm create astro@latest my-store
cd my-store
npx astro add node        # or: vercel, cloudflare, netlify
npm install @dscodotco/sdk
astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
 
export default defineConfig({
  output: "server",                 // on-demand rendering — required
  adapter: node({ mode: "standalone" }),
});

Set the environment variables

.env
FLIGHTDECK_API_URL="https://api.ruo.pro"
FLIGHTDECK_TENANT="ruo-demo"
FLIGHTDECK_STOREFRONT_TOKEN="your-storefront-token"

Astro exposes only PUBLIC_-prefixed variables to the client. Never prefix the token PUBLIC_; read it through import.meta.env in server code only.

Construct the client server-side

src/lib/store.ts
import { createStorefrontClient } from "@dscodotco/sdk";
 
export const store = createStorefrontClient({
  apiUrl: import.meta.env.FLIGHTDECK_API_URL,
  tenant: import.meta.env.FLIGHTDECK_TENANT,
  storefrontToken: import.meta.env.FLIGHTDECK_STOREFRONT_TOKEN,
});

Render the catalog in a page

Frontmatter runs on the server, so the SDK call is safe here. Render image_url directly.

src/pages/index.astro
---
import { store } from "../lib/store";
const { products } = await store.products.list();
---
<ul>
  {products.map((p) => (
    <li>
      <a href={`/product/${p.slug}`}>
        {p.image_url && <img src={p.image_url} alt={p.name} width="240" />}
        {p.name}
      </a>
    </li>
  ))}
</ul>

Build the PDP with an honest 404

Return Astro’s Astro.redirect or a 404 Response for a real not-found; rethrow anything else so a failed read never renders as an empty page.

src/pages/product/[slug].astro
---
import { FlightdeckError } from "@dscodotco/sdk";
import { store } from "../../lib/store";
 
let product;
try {
  ({ product } = await store.products.get(Astro.params.slug!));
} catch (err) {
  if (err instanceof FlightdeckError && err.status === 404) {
    return new Response("Not found", { status: 404 });
  }
  throw err; // 5xx / transport — do not fabricate an empty product
}
const variant = product.variants[0];
---
<main>
  <h1>{product.name}</h1>
  <p>${(variant.price_cents / 100).toFixed(2)}</p>
  <form method="post" action="/api/checkout">
    <input type="hidden" name="variantId" value={variant.id} />
    <input name="email" type="email" placeholder="Email" required />
    <input name="ccnumber" placeholder="Card number" required />
    <input name="ccexp" placeholder="MM/YY" required />
    <input name="cvv" placeholder="CVV" required />
    <button type="submit">Buy now</button>
  </form>
</main>

Add the checkout endpoint (the BFF)

An Astro endpoint is a server route. It receives the posted form and calls the SDK with a deterministic idempotency key.

src/pages/api/checkout.ts
import type { APIRoute } from "astro";
import { FlightdeckError } from "@dscodotco/sdk";
import { store } from "../../lib/store";
 
export const POST: APIRoute = async ({ request, clientAddress }) => {
  const form = await request.formData();
  const email = String(form.get("email"));
  const variantId = String(form.get("variantId"));
  try {
    const result = await store.checkout.submit(
      {
        customer_ref: `guest:${email}`,
        items: [{ variant_id: variantId, quantity: 1 }],
        card: {
          ccnumber: String(form.get("ccnumber")),
          ccexp: String(form.get("ccexp")),
          cvv: String(form.get("cvv")),
        },
        email,
        ip: clientAddress,
      },
      { idempotencyKey: `buynow-${variantId}-${email}:attempt-1` },
    );
    return Response.redirect(new URL(`/order/${result.order.id}`, request.url), 303);
  } catch (err) {
    if (err instanceof FlightdeckError) {
      return new Response(JSON.stringify({ error: err.body }), {
        status: err.status,
        headers: { "content-type": "application/json" },
      });
    }
    throw err;
  }
};

For a multi-line cart, keep { variantId, quantity } in client state or a cookie and post the array to this endpoint; derive the idempotency key from a stable cart id plus attempt. The platform re-prices every line at submit — you never send a total. See Integrate checkout.

Deploy

Run the standalone Node build on any Node host. Set the three env vars in the host’s secret store.

What this guide does not cover

  • Catalog and collection writes — merchant-authored in the console; the SDK is read plus checkout only.
  • Token provisioning — the storefront token (an opaque secret, no fixed prefix, platform-wide today) is issued to you by the platform operator.