App Proxy (Shopify App Proxy)
Definition
An app proxy routes requests for a path under the store's own domain through Shopify to your app's server. What the shopper sees in the browser stays on the same domain as the storefront they are already on, such as my-brand.com, so external data can be displayed without running into CORS or third-party cookie restrictions.
How a request is forwarded
Say a review app declares https://reviews.example.com/proxy as its destination, with apps as the prefix and reviews as the subpath:
What the shopper requests
https://my-brand.com/apps/reviews/list?product_id=123
What the app's server receives
https://reviews.example.com/proxy/list?product_id=123&shop=my-brand.myshopify.com&logged_in_customer_id=456&path_prefix=%2Fapps%2Freviews×tamp=1789200000&signature=…
The /apps/reviews part of the path is swapped for the destination URL, everything after it and the query string are carried through, and Shopify appends four parameters plus the signature.
Configuration
Declare the destination URL, a prefix and a subpath under [app_proxy] in shopify.app.toml. The prefix must be one of a, apps, community or tools. It needs the write_app_proxy scope, and an app can have only one proxy route.
What Shopify adds
Five parameters: shop, logged_in_customer_id, path_prefix, timestamp and signature.
To verify signature, drop it, sort and concatenate the remaining query parameters, and compute an HMAC-SHA256 with the app's client secret as the key. Unlike a webhook signature, which is Base64 in a header, this one is hex encoded and arrives as a query parameter.
Responding
Cookies are unavailable. Shopify strips the Cookie header from the request and Set-Cookie from the response, so a cookie-based session is not an option.
Set Content-Type: application/liquid and Shopify renders the Liquid in the response against the shop's theme.
Background
Why use one
An app proxy lets your own backend be called from the storefront's own domain. In the example above, the URL in the shopper's browser stays on my-brand.com while the response actually comes from your server.
Compared with calling an API on another domain directly, it sidesteps CORS and third-party cookie restrictions, and to the shopper it reads as part of the store.
What the signature does and does not prove
The most important caveat is that verifying the signature is not enough.
Shopify computes the signature at the moment it forwards the request, over the whole query string as it stands. Parameters the shopper's browser added are signed no differently from the ones Shopify appends. So a logged-in user who requests ?customer_id=999 gets that value signed and forwarded along with everything else. The signature that reaches the app is genuine and verification passes.
What the signature guarantees, then, is that the request arrived through Shopify and was not altered in transit. It says nothing about whether the parameter values are legitimate, because the user chose them.
Only one identity can be trusted
The only trustworthy identity in the request is logged_in_customer_id. Shopify sets it from the login session and the user cannot supply it.
Consider what happens if your app instead decides whose data to return from its own customer_id parameter. Anyone who is merely logged in can request someone else's ID, pass signature verification, and receive that person's order history or personal details. It is a routine broken-access-control bug, and the documentation says as much: a session token's sub claim can be trusted, a ?customer_id= query parameter cannot.
Two things to do about it. Always check that the owner of the requested data matches logged_in_customer_id. And treat the empty string, which is what the parameter holds when nobody is logged in, as unauthenticated rather than falling through to a default.
Guarding against replay
The signature covers timestamp, so it cannot be forged, but there is no expiry built in. To stop an attacker from capturing a legitimate request and replaying it later, the app has to check that the timestamp is recent. The comparison itself should use a timing-safe function.
The peer address in your logs is Shopify
The address your server logs is Shopify, not the shopper, because Shopify is what opens the connection. The originating client IP arrives in the X-Forwarded-For header, and the domain the shopper actually had open in X-Forwarded-Host.
Any log analysis, geolocation or abuse tracking that needs the client IP has to read those headers rather than the socket address, which would otherwise collapse all traffic onto a handful of Shopify addresses.
Headers can be set by anyone, though, so X-Forwarded-For is only trustworthy once the signature has established that the request really came through the proxy.
Rate limiting comes in two layers
Requests to a proxy endpoint pass through Shopify's infrastructure first, so the protections that apply to Shopify-hosted storefront pages apply here too. Shopify imposes strict rate limits on unsigned automated traffic and answers requests it judges malicious with a 430 Shopify Security Rejection. Compared with exposing a backend directly to the internet, that layer is a real advantage.
But no threshold is published and none of it is tunable from the app, so treat the platform's protection as a floor and implement whatever limits the business actually needs in the app. Throttling by peer address will not work there, since that address is Shopify; key it on X-Forwarded-For or logged_in_customer_id instead.
Important constraints
- Merchants can change the prefix and subpath: they can edit both in the admin, and the change takes effect immediately. Do not hardcode either; read
path_prefixinstead. Changing the declared values in the app affects only new installations, not existing ones. - Calling from a UI extension changes the rules: both checkout UI extensions and customer account UI extensions can call an app proxy, but they run in a sandboxed Web Worker. They need
network_accessdeclared, which requires a request in the Partner Dashboard that is automatically approved, and the request becomes a CORS request. An extension's origin can change without notice, which is why Shopify requires the server to returnAccess-Control-Allow-Origin: *. Narrowing it to the checkout host does work in practice, which we have verified, but the reason for the wildcard is that the host may change later, so published apps should follow it.logged_in_customer_idis not set either, so identify the logged-in customer from the session token'ssubclaim. - Password-protected stores do not support this route from a UI extension: extension requests come from a Web Worker that does not share a session with the parent window.
- Some HTTP methods are unavailable:
CONNECTandTRACEare unsupported.
Flagship's Involvement
We use app proxies wherever a storefront needs to reach our own backend, including in Mypage Blocks, one of the apps we publish.
Beyond verifying the signature, our implementations are required to check any identifier in the request against logged_in_customer_id. Passing signature verification and being entitled to the data are two different things, and we hold that line in implementation and in review. Validating the timestamp is an explicit task as well, as a guard against replay. We treat flood protection as outside the proxy's remit, and add throttling in the app on the routes that need it.
When a signature fails to match, we work through a standard set of causes: the wrong secret, the proxy configured on a different app than expected, requests reaching the backend without passing through the proxy at all, and query parameters changing between signing and verification.
Internally we run security training scoped to working with Shopify's systems, and app proxy signature verification is one of its topics.