Documentation menu

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)

  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 — it 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 — edge-safe, no node:crypto) and a constant-time comparison. Failure → 401 JSON ({ "error": "invalid_signature" }). Content is never served on an unverified proxy requestverifyAppProxySignature(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 — strips pathPrefix and Shopify’s injected params (merchant query params are preserved), host 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 — there is 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, standard event emitted
Missing or invalid signature401 { "error": "invalid_signature" }
Signed timestamp missing, malformed, or stale401 { "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

Scope