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).
Collections & faceted search
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.