Hi all — I’m building a checkout UI extension on api_version = "2026-04" (Preact + web components) that needs to call my app’s backend to check the buyer’s custom role before allowing them through checkout. In local dev (shopify app dev with a stable Tailscale tunnel), every fetch from inside the extension fails with:
Error: Fetches to the iframe's origin are not allowed.
I’ve narrowed it down enough to be confident the code itself is OK — I’m hoping someone can point me at the correct pattern for 2026-04 checkout extensions calling their own backend in dev.
What the extension does (minimal)
tsx
import '@shopify/ui-extensions/preact';
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useBuyerJourneyIntercept } from '@shopify/ui-extensions/checkout/preact';
declare const shopify: any;
export default function extension() {
render(<Extension />, document.body);
}
function Extension() {
const [status, setStatus] = useState<'loading' | 'allow' | 'block'>('loading');
const companyId = shopify?.buyerIdentity?.purchasingCompany?.current?.company?.id;
useEffect(() => {
(async () => {
try {
const token = await shopify.sessionToken.get();
const base = new URL(shopify.extension.scriptUrl).origin; // <-- tunnel origin
const res = await fetch(`${base}/api/get-role`, {
headers: { Authorization: `Bearer ${token}`, 'X-Company-Id': companyId },
});
const { customRole } = await res.json();
setStatus(customRole === 'Buyer 1' ? 'block' : 'allow');
} catch (e) {
console.log('fetch failed', e); // <-- "Fetches to the iframe's origin are not allowed"
setStatus('allow');
}
})();
}, [companyId]);
useBuyerJourneyIntercept(({ canBlockProgress }) =>
canBlockProgress && status === 'block'
? { behavior: 'block', reason: 'Approval required', errors: [{ message: 'Approval required' }] }
: { behavior: 'allow' }
);
return <></>;
}
Extension TOML
toml
api_version = "2026-04"
[[extensions]]
type = "ui_extension"
handle = "checkout-approval-gate"
[[extensions.targeting]]
module = "./src/Extension.tsx"
target = "purchase.checkout.actions.render-before"
[extensions.capabilities]
api_access = true
network_access = true
block_progress = true
[[extensions.metafields]]
namespace = "my_namespace"
key = "approval_id"
Confirmed working
-
Extension iframe origin (from console errors) is
https://extensions.shopifycdn.com. -
companyIdresolves correctly (gid://shopify/Company/...). -
useBuyerJourneyInterceptfires withcanBlockProgress: true— the gate IS active in the checkout editor with Allow app to block checkout turned on. -
The extension is correctly placed in the checkout editor and rebuilds on save.
-
The bundle is well under 64 KB.
-
shopify.appMetafields.valuereturns[](expected — no approval_id set yet on a fresh cart).
What I’ve tried
-
Direct fetch to
<tunnel>/api/get-role— fails withFetches to the iframe's origin are not allowed. The request never reaches my backend. -
App proxy — added
[app_proxy]toshopify.app.tomlwith my tunnel URL + subpath, calledhttps://<shop>.myshopify.com/apps/<subpath>/get-role/app-proxied. Hit CORS issues -
Downgrading
api_versionto2026-01— sameFetches to the iframe's origin are not allowederror, so this isn’t strictly a 2026-04-only thing.
A working checkout-blocker-limited-buyer extension in our other project (GIC) does exactly this same flow on api_version = "2025-07" (React + non-web-components) without any issues, so something changed in the sandbox between then and 2026-01+.
Setup
-
Shopify CLI
3.94.3 -
Node 22
-
Stable HTTPS tunnel (Tailscale Funnel) so the URL doesn’t rotate between restarts
-
Dev store with B2B enabled, app installed, checkout extension activated in the editor with
block_progress = true
Questions
-
In
2026-04+ checkout UI extensions, what’s the supported pattern for calling the app’s own backend from the extension in dev mode? Is there a CLI flag orshopify.app.tomlblock I’m missing that makes the iframe origin distinct from the backend origin in dev? -
If app proxy is the answer, what’s the correct way to register it for a checkout UI extension (vs. a storefront block)? The docs I found mostly cover storefront usage.
-
Is there a way to add CORS headers / allowed origins so direct cross-origin fetches from
extensions.shopifycdn.comto the dev tunnel work?
Any pointers appreciated — happy to share more code or repro.
Thanks!