Developer guide
When app proxies do not work for Shopify UI extensions
An explanation of why Shopify app proxies are the wrong integration path for many UI extension scenarios, especially in checkout and customer account surfaces where session tokens and extension APIs are the real auth model.
Why the mental model breaks
The bug usually starts life as a very normal sentence: “We already have an app proxy, so the extension can just call that.” It sounds efficient. It sounds tidy. It sounds like architecture. Then checkout shows up with its own runtime rules, customer account extensions show up with their own auth model, and your supposedly simple reuse turns into a thrilling museum tour of CORS headers, missing identity, and dev stores that suddenly serve a password page instead of your JSON.
The core mistake is not technical first. It is conceptual. App proxies belong to the online store. Checkout and customer account UI extensions do not. They are Shopify-hosted extension surfaces with explicit capabilities, explicit APIs, and explicit backend auth expectations. Reusing a storefront transport because it already exists is like reusing a canoe as a server rack. Both are real objects. That is where the similarity ends.
Shopify’s own checkout UI extension configuration docs spell out the most famous symptom. If a UI extension makes a request to an app proxy on a password-protected shop, that path is unsupported. This is not an obscure corner case. It is Shopify telling you, in polite documentation language, that the mental model already drifted.
“UI extension requests made to the App Proxy of password protected shops is not supported.”
The working rule
If the request exists because the buyer is in checkout or in customer accounts, use the auth model of that surface. Do not drag online-store transport rules into a non-storefront runtime just because the endpoint already exists.
What app proxies actually are
App proxies are not “Shopify’s generic way to reach your backend.” They are an online-store
forwarding mechanism. Shopify takes a request to a shop-domain path, forwards it to your app,
and appends proxy-specific information such as shop, path_prefix,
timestamp, signature, and, for storefront traffic, potentially
logged_in_customer_id. Shopify also strips cookies and some headers on the way
through.
That tells you what job app proxies were hired to do. They solve a storefront problem: letting the online store ask your app for dynamic behavior while keeping the request under a shop-domain route. They are not a secret tunnel that makes every Shopify surface behave like Liquid with better branding.
“App proxies don’t support cookies.”
| Property of app proxies | What it means in practice | Why it matters architecturally |
|---|---|---|
| Lives under a shop-domain storefront path | Your request starts from the online store URL space | It is naturally aligned with theme and storefront behavior |
| Uses proxy query parameters and HMAC signature | Your server verifies the forwarded proxy request shape | Identity and trust are tied to proxy semantics, not extension semantics |
Can include logged_in_customer_id on storefront traffic | Storefront identity can piggyback on the proxy request | That assumption breaks for UI extension proxy requests |
| Cookies are stripped | You cannot lean on normal same-origin cookie habits | It is not a transparent browser pass-through |
| Merchant-facing path customization exists | The proxied path prefix is part of storefront configuration reality | Again: this is an online-store feature, not a universal app transport |
Once you see app proxies as storefront plumbing, a lot of confusion disappears. They are good when the shop’s online store is the natural caller. They are awkward when the real caller is a checkout worker, a customer account extension runtime, or any surface whose security model is not “forward this storefront request to my server.”
Where app proxies stop fitting UI extensions
Shopify does allow UI extensions to request app proxy URLs. That is the detail that traps a lot of teams. “Allowed” is not the same as “architecturally correct,” and Shopify documents multiple caveats that tell you exactly why.
UI extension requests to app proxies execute as CORS requests, so your backend must behave like a public cross-origin API rather than a cozy little same-origin storefront endpoint.
UI extension requests to app proxies do not get the
logged_in_customer_idquery parameter assigned.UI extension requests to app proxies on password-protected shops are unsupported.
Your API server cannot assume that every request hitting a public extension endpoint truly originated from your extension just because the route name sounds confident.
“Instead use a session token which provides the sub claim for the logged in customer.”
That one sentence does most of the architecture work. If your current plan depends on
logged_in_customer_id , signed proxy parameters, or any storefront-only request
assumptions, then your plan is already built on the wrong identity model. Checkout and
customer account surfaces do not want you guessing identity from the URL. They want you
validating token claims.
Shopify’s security notes for UI extension networking push the same point harder. A session token can let your server trust the integrity of claims such as the subject or shop, but it does not mean the HTTP request itself is magically trustworthy. That means narrow endpoints, explicit authorization, and zero romance about client-provided identifiers.
“You cannot guarantee that your own extension will have made every request.”
In other words: if you expose /get-discount-code and trust whoever calls it
because they sent you a nice-looking body payload, you have not built an extension backend.
You have built a coupon piñata.
Where app proxies still make sense
This is not an anti-proxy manifesto. App proxies are still useful when the online store is the actual surface that needs the data or behavior.
- Theme-side widgets that need dynamic data under a shop-domain path.
- App embeds or app blocks that fetch public or semi-public storefront data.
- Storefront personalization where the browser is already operating in the online store.
Small server-rendered or JSON-backed storefront features where the proxy route is part of the buyer-facing product behavior.
In those cases, the proxy is not a workaround. It is the feature. The shop-domain URL, the forwarded request semantics, and the proxy signature are all part of the intended design.
The trouble starts when teams promote app proxy from “useful storefront tool” to “our one true backend ingress for everything.” That is how you end up routing checkout logic through a transport built for storefront forwarding and then acting shocked that it smells like storefront forwarding.
| Surface | Natural backend pattern | App proxy fit |
|---|---|---|
| Online store theme code | App proxy or another public storefront-safe endpoint | Often good |
| Theme app extension JavaScript behavior | Often app proxy when shop-domain routing matters | Often good |
| Checkout UI extension | Direct backend endpoint with session token verification | Usually wrong or at least awkward |
| Customer account UI extension | Direct backend endpoint with session token verification | Usually wrong or at least awkward |
What checkout and account extensions need instead
The standard pattern for checkout and customer account extensions is smaller, stricter, and better. You do not need to invent a grand transport layer. You need a purpose-built extension endpoint.
- Enable network access for the extension surface that needs it.
- Get a session token from the extension runtime.
- Send that token to your backend in the
Authorizationheader. - Verify the token server-side.
Authorize the specific action using trusted claims such as the shop domain and, when present, the subject claim.
- Return only the minimum data the extension actually needs.
Shopify’s platform and library docs make this boundary explicit. In the Shopify app libraries, checkout requests, customer account requests, and app proxy requests do not share the same authentication entrypoint. That is not accidental API design. It is the platform telling you that these are different trust boundaries.
| Use case | Backend auth path | Primary trust input | Common mistake |
|---|---|---|---|
| Storefront proxy request | authenticate.public.appProxy() or manual proxy verification | Proxy signature and proxy params | Assuming this model should also front checkout and account traffic |
| Checkout UI extension request | authenticate.public.checkout() or verified session token | Session token claims | Depending on proxy query params or storefront identity rules |
| Customer account extension request | authenticate.public.customerAccount() or verified session token | Session token claims | Treating account UI like a theme widget with better CSS |
There is another important consequence here. Once you stop forcing everything through an app proxy, your backend design usually gets cleaner. Instead of one vague, overloaded, public-ish route that tries to understand every Shopify surface, you create one or two narrow endpoints per extension concern. That makes authorization review easier, testing easier, logs easier, and future-you less likely to whisper threats at the monitor.
“Provides access to session tokens, which can be used to verify token claims on your app’s server.”
A practical request flow that does work
The practical version looks like this: the extension asks Shopify for a session token, sends it to your backend, your backend verifies it, and then your backend performs app-owned logic for the trusted shop and subject context. No storefront proxy workaround required.
1. In the extension, fetch a session token and call your backend directly
const token = await api.sessionToken.get();
const response = await fetch("https://app.example.com/api/extensions/offers", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({
lineIds: api.lines.current.map((line) => line.id),
}),
});
if (!response.ok) {
throw new Error("Extension backend request failed");
}
const data = await response.json();This keeps the extension thin. It sends only the app-relevant input and the platform-issued token. It does not invent customer identity in the payload, it does not smuggle storefront query parameters around, and it does not pretend the shop domain path is the important thing.
2. Enable CORS deliberately on the backend
Shopify documents that UI extensions run in a web worker and that your server must support CORS for extension-originated requests. In Rails, that usually means making the extension API explicit instead of opening the whole castle gate and hoping nobody notices.
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*"
resource "/api/extensions/*",
headers: %w[Authorization Content-Type],
methods: %i[options post]
end
endYes, the wildcard origin can feel rude. Shopify is explicit here because extension origins can change without notice. The defensive move is not to fight that rule. The defensive move is to keep the endpoint narrow and verify the token properly.
3. Verify the session token on your server
If you are on Shopify’s React Router or Remix stack, use the provided public authentication helpers for checkout and customer account requests. They validate the decoded session token and help you return the required CORS headers.
export const action = async ({request}) => {
const {cors, sessionToken} = await authenticate.public.checkout(request);
const shopDomain = new URL(sessionToken.dest).host;
const subject = sessionToken.sub;
const payload = await runAppLogic({shopDomain, subject});
return cors(Response.json(payload));
};On Rails or another custom backend, verify the JWT using your app secret, verify audience,
verify timing claims, and then resolve the shop from dest. Treat
sub as trusted subject context when that claim is present for the surface you are
handling.
# app/services/shopify_extension_session_token.rb
class ShopifyExtensionSessionToken
class InvalidToken < StandardError; end
def self.decode!(raw_token)
payload, = JWT.decode(
raw_token,
ENV.fetch("SHOPIFY_API_SECRET"),
true,
algorithm: "HS256",
aud: ENV.fetch("SHOPIFY_API_KEY"),
verify_aud: true,
verify_expiration: true,
verify_not_before: true
)
shop_domain = URI(payload.fetch("dest")).host
raise InvalidToken, "Unknown shop" unless Shop.exists?(shopify_domain: shop_domain)
payload
rescue JWT::DecodeError, JWT::ExpiredSignature, URI::InvalidURIError, KeyError => e
raise InvalidToken, e.message
end
end# app/controllers/api/extensions/offers_controller.rb
class Api::Extensions::OffersController < ApplicationController
skip_forgery_protection
def create
payload = ShopifyExtensionSessionToken.decode!(bearer_token)
shop = Shop.find_by!(shopify_domain: URI(payload["dest"]).host)
render json: {
shop: shop.shopify_domain,
subject: payload["sub"],
offers: ExtensionOfferResolver.call(shop: shop, subject: payload["sub"])
}
rescue ShopifyExtensionSessionToken::InvalidToken
head :unauthorized
end
private
def bearer_token
request.authorization.to_s.sub(/\ABearer\s+/i, "")
end
endThe most important part is not the gem or the framework. It is the boundary discipline: resolve shop from verified token claims, compare any sensitive subject context against the token, and keep the route specific enough that a random caller cannot turn it into a vending machine for privileged data.
4. Keep the endpoint purpose-built
Good extension endpoints usually look boring on purpose:
POST /api/extensions/offersPOST /api/extensions/eligibilityPOST /api/extensions/order-summary
Bad extension endpoints often look “reusable”:
GET /proxy/dataPOST /public/extensionGET /get-discount-code
Reusability is nice. Trust boundaries are nicer.
How to spot the wrong architecture early
You can usually detect the bad design before the first bug report if you ask one question: what platform-issued thing does the backend trust here?
If the answer is “well, the extension passes a customer id in the body” or “we verify the proxy signature because that is what our other route does,” you are already standing in the wrong building.
Your extension backend plan depends on storefront query parameters, proxy signatures, or a shop-domain proxy path.
Your checkout development flow mysteriously breaks on password-protected stores.
Your endpoint needs to guess whether the caller was theme code, checkout, customer account, or some other client because everything shares one public route.
Your authorization story depends more on request shape than on verified token claims.
Your endpoint returns sensitive data to any request that knows the shop domain and a little optimism.
Those are not random bugs. They are architecture smells. Fix the transport boundary and most of the downstream weirdness gets dramatically easier.
The shortest decision rule
Use app proxies for online-store behavior. Use session-token-backed endpoints for checkout and customer account UI extensions. The number of exceptions is much smaller than the number of people trying to invent them.
Best internal links
Sources and further reading
Shopify Dev: Checkout UI extensions configuration
Shopify Dev: Authenticate app proxies
Shopify Dev: About session tokens
Shopify Dev: Customer Account UI extensions session token API
Shopify Dev: authenticate.public.checkout
Shopify Dev: authenticate.public.customerAccount
Shopify Dev: authenticate.public.appProxy
Shopify developer changelog: Session Token API for checkout UI extensions
FAQ
Can a Shopify UI extension call an app proxy at all?
Yes, but that does not make it the right architecture. Shopify explicitly documents that UI extension requests to app proxies are CORS requests, do not get logged_in_customer_id assigned, and are unsupported on password-protected shops.
Why does an app proxy often fail on development stores for checkout work?
Because Shopify documents that UI extension requests to an app proxy on password-protected shops are not supported. Extensions run in a web worker and do not share the parent window session used by the password page.
What should a checkout or customer account extension use instead of an app proxy?
Use the extension's session token API, send the token to your backend, verify it server-side, and keep the endpoint narrowly scoped to the action or data your extension needs.
When is an app proxy still a good fit?
When the request belongs to the online store itself, such as storefront rendering, theme JavaScript, app embeds, app blocks, or shop-domain storefront widgets where the proxy path is part of the product behavior.
Related resources
Keep exploring the playbook
Session tokens for Shopify UI extensions explained
A guide to session tokens in Shopify UI extensions, with the differences between embedded app auth, checkout extensions, and customer account extensions explained in architecture terms that matter in production.
Calling a Rails API from a Shopify customer account extension
A practical guide to calling Rails endpoints from a Shopify Customer Account UI Extension, including session-token verification, endpoint design, and the requests that should not go through your backend at all.
Checkout UI Extensions with a custom backend architecture
A practical backend architecture guide for Shopify Checkout UI Extensions, covering when to call your own backend, how to keep the extension thin, and where teams overbuild the server side.