Documentation menu

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)

  1. 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.
  2. 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.
  3. Deploy the handler on any web-standard runtime:
app/apps/rebilder/[[...path]]/route.ts
// 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:

  1. Verifies the signature with Web Crypto (crypto.subtle, which is edge-safe with no node:crypto) and a constant-time comparison. Failure → 401 JSON ({ "error": "invalid_signature" }). Content is never served on an unverified proxy request; verifyAppProxySignature(url, sharedSecret) is exported for reuse.
  2. Checks timestamp freshness: a signed timestamp older than 90s (±5s clock-skew tolerance) → 401 JSON ({ "error": "stale_timestamp" }), bounding the replay window of a captured signed URL.
  3. Only then reconstructs the canonical storefront URL (stripping pathPrefix and Shopify’s injected params while preserving merchant query params, with the host taken from the signed shop param) and runs the core handleRequest. 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:

RequestResponse
Verified agent request, a source matched200 text/markdown: the standard gateway response, with the standard event emitted
Missing or invalid signature401 { "error": "invalid_signature" }
Signed timestamp missing, malformed, or stale401 { "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

Scope