Shopify adapter
Serve gateway markdown from a merchant’s own storefront domain through a Shopify App Proxy, with no theme edits, no Liquid, and HMAC verification.
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 while the serving lives with you.
There are no theme edits, no Liquid, and no new dependencies, because 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, which 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, which is edge-safe with 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 (stripping
pathPrefixand Shopify’s injected params while preserving merchant query params, with the host taken 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, with 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, with the standard event emitted |
Missing or invalid signature | 401 { "error": "invalid_signature" } |
Signed timestamp missing, malformed, or stale | 401 { "error": "stale_timestamp" } |
Anything else: an unmatched URL, a source that returned null or threw, or a non-agent requester (human/crawler hitting the proxy subpath) | 404 { "error": "no_source" }, since the storefront’s real pages live at their canonical URLs |