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/sdkimport { 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
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
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.
---
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.
---
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.
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.
Related
- SDK reference — every namespace and method.
- Integrate checkout — the full checkout flow.
- Build your own frontend — the BFF pattern in any framework.