Technical overview: the Embed Migration Kit and Cutover
This page is for your lead developer and anyone on the technical side who was not on the call. It explains the two tools I built for the script tag migration: how they are built, how they are tested and deployed, and what they deliberately do not do.
- Embed Migration Kit is a Shopify app. It moves one store at a time from a script-tag integration to a theme app embed, and it proves the embed really runs before removing anything.
- Cutover is an installable web app (PWA) for the people who run the migration. It has the manuals, the deadlines, a diagnosis tree, browser-only tools and a per-store tracker.
Part 1 · Embed Migration Kit
What it is
The kit is an embedded admin app built on Shopify's React Router app template. The merchant opens it from the Shopify admin. The kit reads the store's script tags and theme files, tells the merchant exactly what to switch on and where, verifies the result with three independent signals, and only then cleans up. It also moves "one variant per country" pricing to Markets price lists.
Stack
| Layer | What it uses |
|---|---|
| App framework | Shopify app template for React Router 7 (@shopify/shopify-app-react-router v3), Node.js 22 |
| Admin UI | Embedded in the Shopify admin through App Bridge, built with Polaris web components (<s-section>, <s-table>, <s-button>) |
| Shopify APIs | Admin GraphQL API 2026-10: scriptTags, scriptTagCreate (only as a live proof), scriptTagDelete, themes / theme.files, Markets catalogs, priceListCreate, priceListFixedPricesAdd, contextualPricing |
| Storefront | Theme app extension with one app embed block (target: body): a country selector that posts Shopify's native localization form, plus a beacon |
| Data | Prisma with SQLite: sessions (including refresh tokens), beacons, extension status snapshots and the removal queue |
| Auth | Expiring offline access tokens with refresh, which is what the Admin API requires from January 1, 2027 |
| Webhooks | app/uninstalled, app/scopes_update and the three compliance topics (customers/data_request, customers/redact, shop/redact) |
How the data flows
The five stages, per store
Measure. Lists the script tags this app still has registered and scans every theme (
layout/theme.liquid,snippets/*,sections/*) for legacy currency code: prices wrapped for JavaScript conversion, old currency pickers and included legacy snippets. Every hit has a file and a line. A "live proof" button asks Shopify to create a script tag and shows the refusal word for word.Activate. Reads
config/settings_data.jsonfor each theme. It strips the leading comment that breaksJSON.parse, handlescurrentbeing a preset name and matches the block by handle. Then it builds the activation deep link by theme ID, neverthemes/current, with manual steps as a fallback.Verify. Enabled is not the same as running. The kit waits for three signals:
- A · the embed is on in
settings_data.jsonof the live theme; - B ·
app.extensions()reports it active while the merchant is in the app, stored with a timestamp because it only answers then; - C · a beacon from the embed, sent through the app proxy, when it actually runs on a storefront page.
- A · the embed is on in
Remove. A per-store queue deletes this app's script tags one at a time, only after all three signals are green. It backs off on throttling, using
throttleStatus. On423 Lockedit waits 15 minutes, doubling up to 6 hours. Frozen (402), closed (404) and token-rejected stores go to a separate "unreachable" list instead of failing the run. Legacy theme code gets a human cleanup checklist with file, line and "already neutralized by the embed".Prices by country (only when needed). Finds products that fake a local price with one variant per country and moves the price to Markets:
- the market's catalog, created if missing and without a publication, so availability stays with the sales channel;
- a price list in the market's currency;
- fixed prices in batches of at most 250 (
priceListFixedPricesAdd), retried on throttling; - an audit with
contextualPricing(context: {country}), which compares what Shopify will actually charge with the plan.
It is idempotent and never deletes variants or products.
Every store also gets a tokens card, showing it runs with expiring offline tokens, and a JSON and CSV report, so many stores can be merged into one migration board.
Design decisions worth knowing
- Three signals before any deletion. If a theme has
</body>inside a comment, embeds can be "enabled" and never run. The beacon catches that. - Deep link by theme ID. Activation is per theme. Linking to
currenthas opened the wrong theme in the past. write_script_tagsstays in the app's scopes until the audit reads zero tags in every reachable store. Otherwise a re-authorization without that scope makes cleanup impossible.- No
write_themes. A public app cannot rewrite theme files without an exemption. Legacy code is neutralized from the embed and cleaned by a person, on a duplicated theme, with written approval. - One store at a time. Bulk operations are per store anyway; the hard part across thousands of stores is orchestration and the 402/423/404 cases, not throughput.
What it cannot do, by design
- Switch the embed on for the merchant: Shopify requires the merchant's click.
- Rewrite theme code from a public app.
- Delete script tags on frozen, locked or closed stores. They are listed with the cause.
- Delete legacy country variants. The merchant decides, after checking orders.
Data and privacy
The kit stores no customer data. A beacon records the shop, the theme ID, name and role, the page path and a count of legacy markers it neutralized. The compliance webhooks are wired in.
How it is tested
npm test # Node.js 22 · 12/12 passing (re-run on October 13, 2026)
The 12 unit tests cover the pure logic in app/lib/:
- legacy markers with file and line;
settings_data.jsonparsing, including the leading comment, presets, disabled blocks, other apps' blocks and broken JSON;- the deep link by theme ID;
- the queue backoff for throttling, 423, 402 and 404;
- the "enabled is not running" verdict;
- detecting one variant per country, the price plan, batches of at most 250 and the price audit.
End to end, the full flow ran on a development store. The embed was switched on, all three signals went green, the selector appeared on the storefront and a product's price in Mexico moved from a converted 16,558 MXN to a fixed 15,999 MXN.
How it is deployed
- One container image based on
node:22-alpine. It runs as a non-root user withno-new-privileges. - SQLite lives on a mounted volume, and migrations run at start.
- It sits behind a reverse proxy with TLS on its own subdomain, alongside other services that share no data with it.
- Releases go out as versioned app versions with the Shopify CLI.
From the first commit to the production release with price lists took under 3 hours of wall-clock time (October 10, 2026).
Part 2 · Cutover
What it is
Cutover is a "migration control room" for the team that runs the migration. It is an installable PWA, in English and Spanish, that works offline. It was built on the morning of October 13, 2026.
What each view does
| View | What it does |
|---|---|
| Board | A countdown to the day script tags stop loading, and a departure board of every dated Shopify platform change since 2024. Each change links to its source and carries a confidence label: official, secondary or community. |
| Manuals | Step-by-step migration manuals with copyable code. A shared "code context" (store, app name, block handle) fills every snippet, so the code is ready to paste. Official facts and field notes are labeled differently. Teams can write their own manuals in Markdown, export them and share them as a link. |
| Fix | "Diagnose a migration": a decision tree that ends in a fix, plus a filterable troubleshooting table of problems developers actually hit (draft themes, 423, settings_data.json, and more). |
| Tools | Tools that run entirely in the browser, so nothing pasted leaves the page:
|
| Stores | The same stages as a per-store checklist, with the runbook's verdicts. Exports to CSV and JSON. |
| Tracker | A dashboard, task list and roadmap for the whole migration. Every task has a tutorial and a "done when" check. It shows progress by phase, the next deadline and recent activity, and exports to CSV. |
How it is built
- Static and client-side only. There is no backend and no server-side storage of what people paste. Progress and team manuals are saved on the device.
- Offline first. A versioned service worker precaches the app shell and the pinned libraries it uses (Prism for code highlighting, JSZip, marked with DOMPurify, lz-string for share links, a QR generator), then serves them offline.
- Safe defaults. It sends
X-Content-Type-Options: nosniff, a strict referrer policy and a permissions policy that turns off camera, microphone and geolocation. The only token in the code is an obvious example value. - Hosting. It is a static site served from its own isolated container behind the same reverse proxy, with TLS.
Next step
Happy to walk your lead developer through the code live.