# [Shopify adapter](https://rebilder.com/docs/adapters/shopify)

> Serve gateway markdown from a merchant’s own storefront domain through a Shopify App Proxy, with no theme edits, no Liquid, and HMAC verification.

- **Updated:** 2026-08-20
- **Publisher:** Rebilder

## 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:

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:

- **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.
- **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.
- 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:

| 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 |

## Managed or self-host

There are two ways to run this adapter, and they serve identically. The steps above are the **self-host** path: your own Partners app, your own deployed endpoint, your own `SHOPIFY_APP_SECRET`, and events wired to the sink yourself. The **managed** path is the Rebilder Shopify app: install it on a store and we run the same serving for you: we host the endpoint, hold the OAuth connection, keep the catalog synced from the store’s own products, pages, collections, and policies, and report agent traffic to the store’s Console automatically.

> **Managed hosting is sold; capability is not** The managed app buys operations, never features: this SDK stays free, ungated, and self-hostable, and both paths produce byte-identical responses. Your store’s [install page](/console/install) shows which path it is on and how to start the other one.