Shopify adapter
Serve gateway markdown from a merchant’s own storefront domain through a Shopify App Proxy — no theme edits, no Liquid, HMAC-verified.
What an app proxy is
Shopify lets an app claim a subpath on the merchant’s own storefront domain (default here: /apps/rebilder/...). Requests to that subpath are forwarded server-side by Shopify to a URL the app hosts, and the response is returned to the requester from the shop’s domain. So an agent fetching https://acme.myshopify.com/apps/rebilder/products/x is answered by this gateway — the agent-facing URL lives on the store, the serving lives with you.
No theme edits, no Liquid, no new dependencies — Web Crypto is ambient in every supported runtime.
Merchant setup (Shopify Partners)
- In the Partners dashboard → your app → Configuration → App proxy: set *Subpath prefix*
apps, *Subpath*rebilder(⇒ storefront path/apps/rebilder), and *Proxy URL* to your deployed handler endpoint. - Copy the app’s Client secret — it is the HMAC key for proxy signatures. Provide it to the handler via env (
SHOPIFY_APP_SECRET); never hardcode it. - Deploy the handler on any web-standard runtime:
// e.g. app/apps/rebilder/[[...path]]/route.ts — any Request/Response runtime works
import { createShopifyAppProxyHandler } from '@rebilder/gateway/shopify'
import { gatewayConfig } from '../../lib/gateway-config'
export const GET = createShopifyAppProxyHandler(gatewayConfig, {
sharedSecret: process.env.SHOPIFY_APP_SECRET!,
pathPrefix: '/apps/rebilder', // as the path arrives at YOUR endpoint; strip is a no-op if absent
})Security model
Shopify appends query params (shop, path_prefix, timestamp, logged_in_customer_id) plus signature: a hex HMAC-SHA256, keyed with the app’s shared secret, over the other params canonicalized as sorted key=value strings concatenated with no separator (values of a repeated key joined with ,). The handler:
- Verifies the signature with Web Crypto (
crypto.subtle— edge-safe, nonode:crypto) and a constant-time comparison. Failure →401JSON ({ "error": "invalid_signature" }). Content is never served on an unverified proxy request —verifyAppProxySignature(url, sharedSecret)is exported for reuse. - Checks timestamp freshness: a signed timestamp older than 90s (±5s clock-skew tolerance) →
401JSON ({ "error": "stale_timestamp" }), bounding the replay window of a captured signed URL. - Only then reconstructs the canonical storefront URL — strips
pathPrefixand Shopify’s injected params (merchant query params are preserved), host from the signedshopparam — and runs the corehandleRequest. Your sources and emitted events see the same canonical URLs (https://{shop}/products/x) as every other adapter.
Response semantics
Because the app proxy is the endpoint — there is no downstream HTML to fall through to — every handleRequest pass-through (null) becomes 404 JSON:
| Request | Response |
|---|---|
| Verified agent request, a source matched | 200 text/markdown — the standard gateway response, standard event emitted |
Missing or invalid signature | 401 { "error": "invalid_signature" } |
Signed timestamp missing, malformed, or stale | 401 { "error": "stale_timestamp" } |
Anything else — unmatched URL, source returned null or threw, or a non-agent requester (human/crawler hitting the proxy subpath) | 404 { "error": "no_source" } — the storefront’s real pages live at their canonical URLs |