ConceptsPerformance & caching

Performance and caching

RUO Pro’s caching story is deliberately small and honest: one conditional revalidation contract on the site read and mechanical CDN URL resolution for assets. Almost nothing else is cached at the platform layer today, and this page says exactly where those lines are so you can build a fast storefront without assuming caching that is not there.

The site read is live, with ETag/304

The store manifest — the authored sections, theme tokens, and nav that define a storefront — is served by two public routes:

  • GET /v1/resolve?hostname= or ?tenant= — the resolution path.
  • GET /v1/tenants/{t}/site — the developer-contract site read (typed in the SDK as store.site.get()).

Both are live (they serve the active, published manifest) and both support conditional revalidation with an ETag:

The ETag is the manifest’s content hash (row_hash = sha256 of the canonicalized manifest), computed at publish time — not updated_at. That means the ETag is stable across re-serves and even across a re-publish of byte-identical content; the only thing that changes it is a manifest whose content actually differs. Send the last ETag you saw as If-None-Match and you get a bodyless 304 whenever nothing changed.

These routes emit no Cache-Control header. Revalidation is purely ETag/304 — there is no platform-set TTL telling a shared cache how long it may serve a stored copy without asking. If you want CDN-layer caching, put a cache you control in front of these routes (see below).

Both routes are fail-closed, never fabricated absence: a read failure is 503 (“resolution failed — this is NOT a missing store”), a genuinely unknown host or tenant is 404, and /v1/tenants/{t}/site returns 404 for any store that is not live (no draft or suspended leak). The preview route (/v1/preview-resolve) is the opposite of cacheable on purpose: it sets cache-control: no-store and emits no ETag, so a preview can never pollute a cache that feeds live rendering.

On the hosted storefront, manifest resolutions are served from cache when possible, with the last successfully-fetched manifest used as a fallback if a live resolve fails — so a transient API hiccup degrades gracefully rather than erroring a live storefront. A genuine 404 (an unpublished or removed store) is never served stale.

Asset and image resolution

Image URLs are resolved mechanically from a content-addressed asset ref — there is no service round-trip and no signed URL on the read path. An asset ref has the shape asset:<tenant>/<sha256>.<ext>, where the extension is derived from the declared content type (never the client filename) and the sha256 addresses the exact bytes, so re-uploading identical bytes is idempotent.

Public catalog responses keep the raw image_asset_ref on each object and add a resolved image_url of the form <assetBaseUrl>/stores/<tenant>/assets/<sha256>.<ext>. If the ref is malformed or unresolvable, image_url is null — honest absence, never a broken URL. Sized webp variants (<sha256>.w{320|640|1280}.webp) are available where an image has been optimized; SVGs never get variants.

Because resolution is a pure rewrite, the CDN serves assets directly and the platform is never in the image read path.

Pagination and its cost

All list endpoints are offset/limit — there is no cursor pagination anywhere today. Non-integer or negative inputs are clamped before they reach SQL, so a malformed ?limit=abc never errors and never runs an unbounded query.

EndpointDefault limitMax limitNotes
Catalog search GET /v1/tenants/{t}/catalog/search2050Max offset 10,000; returns result_count, limit, offset
Merchant customers GET /v1/merchant/customers25100Returns total, limit, offset
Public product list GET /v1/tenants/{t}/catalog/products——Not paginated: returns the full active-product list
⚠️

The public product-list and collection-detail reads are unpaginated and fetch each product’s variants with one query per product (an N+1 pattern). For a large catalog, prefer catalog search (which is paginated and bounded) over listing every product, and lean on the ETag’d site read plus the CDN for the parts of the page that do not change per request.

What is and is not cached

The caching posture, precisely:

  • The manifest site read supports ETag/304 revalidation, so a repeat read that hasn’t changed is a cheap bodyless 304. This is the one read with a caching contract.
  • Catalog and COA reads carry no ETag and no Cache-Control — every one is a fresh origin hit. A non-OK or unparseable catalog read is an error (never degraded to an empty list); a genuine empty result is an honest [].

The hosted storefront reaches the same commerce and inventory routes the SDK exposes — there is no internal back channel — so what you can build against those routes is what the hosted storefront itself runs on.

The honest gaps

  • No platform Cache-Control. Neither the site read nor the catalog reads emit a caching directive. The site read supports ETag/304; the catalog reads support neither ETag nor Cache-Control — every catalog read is a fresh origin hit today.
  • Do not assume an edge cache is in front of these routes. If you want one, put a cache you control in front of them.

Building a fast storefront on top

Practical guidance given the above:

  1. Revalidate the manifest with If-None-Match. Cache the ETag you last saw and send it — a 304 is the cheapest possible refresh, and the manifest only changes on publish.
  2. Cache catalog reads yourself. The platform does not cache them and sets no validators, so put your own cache (your BFF, your framework’s data cache, or a CDN with rules you own) in front of catalog and product reads, keyed by tenant and query.
  3. Let the CDN serve images. Asset URLs resolve straight to the CDN origin; request sized webp variants where they exist. The platform is never in the image path.
  4. Prefer paginated search over full product lists for large catalogs, and bound your limit.
  5. Keep the storefront token server-side. All of these reads are meant to run from your server, not the browser (see Security).