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 asstore.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.
| Endpoint | Default limit | Max limit | Notes |
|---|---|---|---|
Catalog search GET /v1/tenants/{t}/catalog/search | 20 | 50 | Max offset 10,000; returns result_count, limit, offset |
Merchant customers GET /v1/merchant/customers | 25 | 100 | Returns 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/
304revalidation, so a repeat read that hasn’t changed is a cheap bodyless304. 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 norCache-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:
- Revalidate the manifest with
If-None-Match. Cache the ETag you last saw and send it — a304is the cheapest possible refresh, and the manifest only changes on publish. - 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.
- Let the CDN serve images. Asset URLs resolve straight to the CDN
origin; request sized
webpvariants where they exist. The platform is never in the image path. - Prefer paginated search over full product lists for large catalogs, and
bound your
limit. - Keep the storefront token server-side. All of these reads are meant to run from your server, not the browser (see Security).