Guides

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.

Updated March 12, 2026
12 min read
Editorial note: App proxies are useful. Treating them like a universal backend tunnel for every Shopify surface is how developers end up debugging CORS at 2:13 AM while blaming the moon.

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 proxiesWhat it means in practiceWhy it matters architecturally
Lives under a shop-domain storefront pathYour request starts from the online store URL spaceIt is naturally aligned with theme and storefront behavior
Uses proxy query parameters and HMAC signatureYour server verifies the forwarded proxy request shapeIdentity and trust are tied to proxy semantics, not extension semantics
Can include logged_in_customer_id on storefront trafficStorefront identity can piggyback on the proxy requestThat assumption breaks for UI extension proxy requests
Cookies are strippedYou cannot lean on normal same-origin cookie habitsIt is not a transparent browser pass-through
Merchant-facing path customization existsThe proxied path prefix is part of storefront configuration realityAgain: 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_id query 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.

SurfaceNatural backend patternApp proxy fit
Online store theme codeApp proxy or another public storefront-safe endpointOften good
Theme app extension JavaScript behaviorOften app proxy when shop-domain routing mattersOften good
Checkout UI extensionDirect backend endpoint with session token verificationUsually wrong or at least awkward
Customer account UI extensionDirect backend endpoint with session token verificationUsually 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.

  1. Enable network access for the extension surface that needs it.
  2. Get a session token from the extension runtime.
  3. Send that token to your backend in the Authorization header.
  4. Verify the token server-side.
  5. Authorize the specific action using trusted claims such as the shop domain and, when present, the subject claim.

  6. 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 caseBackend auth pathPrimary trust inputCommon mistake
Storefront proxy requestauthenticate.public.appProxy() or manual proxy verificationProxy signature and proxy paramsAssuming this model should also front checkout and account traffic
Checkout UI extension requestauthenticate.public.checkout() or verified session tokenSession token claimsDepending on proxy query params or storefront identity rules
Customer account extension requestauthenticate.public.customerAccount() or verified session tokenSession token claimsTreating 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
end

Yes, 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
end

The 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/offers
  • POST /api/extensions/eligibility
  • POST /api/extensions/order-summary

Bad extension endpoints often look “reusable”:

  • GET /proxy/data
  • POST /public/extension
  • GET /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

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

Guides

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.

guidesShopify developersession tokens