Get startedChangelog

Changelog

Notable changes to the public developer surface — the API, the @dscodotco/sdk client, and the storefront contract. Internal-only changes (console panels, infra scaffolding) are omitted. Dates are grouped by month; where a change alters a contract you consume, the migration note says so.

September 2026

Public catalog now returns a resolvable image_url

The public catalog previously emitted only the opaque asset:<store>/<sha> ref and pushed all resolution to the client. Commerce now resolves the ref and includes image_url (and a per-image url) on products list, product-by-slug, search, and collection detail. Resolution is a pure string transform against an ASSET_BASE_URL; when the base is unset the field is null (honest absence, never a broken guess). The OpenAPI spec and SDK types carry the new field.

Migration: none required — the field is additive. Headless consumers that were resolving asset refs themselves can now read image_url directly.

Replay-safe webhook signatures

Outbound webhooks are now signed with a versioned, replay-safe scheme: x-webhook-timestamp plus x-webhook-signature-v1, where the signature is t=<ts>,v1=<HMAC(secret, ts.body)>. The delivery envelope also gains a created_at. The legacy signature header is still sent but is deprecated. See the webhooks reference for the verification recipe.

Migration: move verification to x-webhook-signature-v1 (timestamp + HMAC over timestamp.body); stop trusting the legacy header.

Public authored-site endpoint — GET /v1/tenants/{tenant}/site

The merchant’s authored site (sections, nav, theme tokens) is now part of the public contract. GET /v1/tenants/{tenant}/site returns the active manifest for a live store — the same ETag contract as store resolve. A non-live or unknown tenant is the same 404 (no existence oracle), and a repo failure is a 503, never a silent empty manifest. The SDK exposes it as store.site.get(). See Manifest & sections and Build your own frontend.

Idempotency-Key on money-path routes

The money-moving routes now take a required, deterministic idempotency key so a retried write replays instead of double-posting. Checkout requires an Idempotency-Key header (a submit without it is a 400); the returns receive (restock + settle refund) route requires it too; the storefront cancel/edit/refund calls take the same key. Make the key deterministic — derive it from the business event, never a fresh random value. The shared mechanism is a new @dscodotco/idempotency package (hash, lookup, execute, insert; canonical sorted-key request hashing; Result-returning throughout). See Money invariants.

Store credit and gift cards as checkout tenders

Two money tenders are now first-class at checkout. Store credit (an authed shopper’s cash balance) applies as a partial tender behind the storeCredit.enabled manifest gate, reducing the card charge by min(balance, orderTotal). Gift cards (a new giftcards module) are purchasable/redeemable bearer codes behind the giftCards.enabled gate — stored tenant-salted-SHA-256 hashed, plaintext shown once at issue, redeemed as an atomic FOR UPDATE partial decrement. A multi-tender order stays zero-sum across store credit + gift card + card. Recipes: store credit, gift cards.

Public, unauthenticated catalog route

Commerce gained the one public, tenant-scoped catalog route the storefront shop pages (and unauthenticated headless callers) need: GET /commerce/v1/tenants/:tenant/catalog/products and .../products/:slug. Read-only, gated three independent ways in code — only status='active' products, only a live store, and a failed read is a 503 — so a draft or suspended tenant 404s identically to an unknown tenant slug (no slug enumeration).

Two discovery surfaces on the public catalog: GET /v1/tenants/:tenant/catalog/search (full-text search with price and collection facets) and .../catalog/collections[/:slug]. Both share the live-store gate and the honest-absence contract; a blank query is a valid “no query” (empty 200), a DB failure is a 503. See the collections & search reference.

Typed SDK — @dscodotco/sdk 0.0.3

@dscodotco/sdk is a typed fetch client generated from openapi.json (ADR-0007). createFlightdeckClient({ baseUrl, fdkToken?, storefrontToken? }) gives compile-time-checked paths, path/query/body shapes, and response types, with the fdk_ Bearer / x-storefront-token credentials injected per the spec’s security schemes; createStorefrontClient(...) wraps it as a tenant-pinned storefront client. The package is now open-source-ready — a README, an MIT license, public publish config, and zero workspace dependencies, so it is publishable and consumable standalone. A spec-vs-app drift test asserts every declared path maps to a real mounted route.

Merchant site builder — drafts, preview, publish, rollback

The manifest is now edited as immutable versioned drafts with an active pointer. New merchant routes (GET /v1/my/site, PUT /v1/my/site/draft, POST /v1/my/site/publish, POST /v1/my/site/preview-token) let a merchant build against the live storefront without touching the published site; publishing an older version is the rollback. A signed, short-lived preview token renders a draft behind a non-dismissible banner and forced noindex. Every publish runs a fail-closed RUO-claims scan over the manifest copy. The portal’s site editor renders the real storefront sections in a drag-and-drop canvas (the same renderSection the live store uses). See Manifest & sections.

Inbound webhooks gateway

The first edges/ network boundary landed: an inbound webhook gateway that verifies a provider signature, dedups on (provider, provider_event_id), stores the raw payload, and publishes one internal webhook.received.v1 event. Fail-closed — unknown provider 404, bad signature 401, a replayed event id returns 200 {received, deduped} with no second internal publish. One reference/test provider is wired; real providers are added at wiring time with their own tenant-resolution rule.