Extending Shopify’s UI without compromising the core

Shopify powers millions of merchants, each with unique workflows. To serve them, the platform relies on third-party developers building apps for marketing automation, sales channels, product sourcing, and more. The challenge is letting those developers integrate deeply with the admin interface while keeping the merchant experience consistent, secure, and fast.

A GIF showing how a 3rd party extension inserting a page highlighting an upsell purchase before the user completes purchase is completed in the Shopify checkout
3rd party extension adding a post-purchase page directly into the Shopify checkout

Our approach needs to satisfy four core requirements:

  • Third-party extensions must match Shopify’s native content in look, feel, performance, and accessibility.
  • Developers should use standard web technologies they already know.
  • Extensions must run securely and reliably without affecting the platform, whether through bugs or malicious code.
  • The experience must remain consistent across web, iOS, and Android.

The core idea: separate UI definition from rendering

The foundation of our solution is a pattern we call remote rendering. The code that defines a UI is separated from the code that actually draws it on screen. These two sides communicate by passing messages, which is ideal because extensions (the UI-defining code) are usually third-party scripts that run in a restricted sandbox, while the host (the rendering code) lives in the main application.

A diagram showing that Extensions define the UI and run in a sandbox and the Host renders the UI and is part of the main application. Extensions and Host communicate via messages between them.
Separating extensions (3rd party code) from host (1st party code)

Host and extension communicate over a MessageChannel. Because all communication is message-based, neither side cares how the other is implemented. At Shopify, this lets us build hosts in three languages—JavaScript for web, plus Kotlin and Swift for native mobile—while keeping a single extension codebase.

The remote-ui library

Remote rendering provides flexibility, but it introduces hard problems: defining an efficient message protocol, implementing function calls over message passing (RPC), and applying UI updates without performance hits. The open-source remote-ui library handles these challenges. Note that in 2024, we renamed remote-ui to Remote DOM; the new library uses the full browser DOM API for UI management in the sandbox, though the RPC layer described here remains essentially the same.

RPC: making functions remote

The @remote-ui/rpc package offers an RPC abstraction with one key feature: functions can be passed across a postMessage interface and called on the other side. This is essential for event callbacks, which are so common in UI code.

Two code snippets displayed side by side showing remote procedure calls using endpoint.expose and endpoint.call
Making remote procedure calls using endpoint.call (script1.js) and endpoint.expose (script2.js)

An endpoint exposes functions for remote calls. Under the hood, the library uses Promise and Proxy objects to hide the details of message passing. It also includes automatic memory management: properties like event handlers are retained while a UI component is mounted and released when it unmounts.

RemoteRoot and RemoteReceiver

The next layer up is RemoteRoot, which gives extensions a DOM-like API for building and modifying a UI component tree. Internally, it serializes updates as JSON messages over RPC and sends them to the host. On the other side, RemoteReceiver receives those messages and reconstructs the component tree locally.

Code snippets showing RemoteRoot and RemoteReceiver working together

With these two pieces, an extension can define UI as a remote tree and the host can rebuild it. What remains is traversing that tree and rendering actual native components.

Rendering to the DOM or into React

remote-ui includes helpers for converting the remote tree to native components. A DomReceiver can render a remote root into a DOM element with minimal setup; it walks the tree, maps remote components to DOM elements, and attaches event handlers automatically (any prop starting with on becomes an event listener).

At Shopify, though, our frontend is React, not raw DOM. For that, the @remote-ui/react package provides a receiver, a controller that maps remote components to their implementations, and a RemoteRenderer component. The component implementations passed to the controller are nothing special—just regular React components.

One noteworthy piece of host-side setup is where third-party scripts are loaded:

// Run 3rd party script in a sandbox environment
// with the receiver as a communication channel ...

Adding a sandbox for security

The high-level architecture has two boxes—extension and host—but production reality adds a third: a sandbox between them.

An image showing the Sandbox as a box surrounding the Extension and a box representing the Host. The two communicate via messages
The sandbox is an additional layer of indirection between the host and the extension

The sandbox runs in an isolated environment (a web worker on the web) and loads extensions safely. It also absorbs boilerplate code, leaving extension developers a cleaner interface. A minimal sandbox lets the host load extension code from a URL, then render it by calling the callback the extension registered. The render arguments provide a remoteChannel for UI updates and an api object exposing native functionality chosen by the host.

For example, the host can pass down a setTitle function in the api. The extension script then calls that function without any awareness of RPC or message passing—it just invokes a property on the object it was given.

A production-ready sandbox

Our simplified sandbox example omits important details like error handling and support for multiple extension callbacks. The production version goes further in restricting the JavaScript environment: some globals such as importScripts are removed entirely, while others like fetch are replaced with safer implementations that only allow requests to approved domains. The sandbox script is also served from a separate domain so the browser enforces additional security boundaries.

Cross-platform support comes from reimplementing this sandbox three times: using web workers on the web, web views on Android, and JavaScriptCore on iOS.

Current adoption and roadmap

The remote rendering approach described here is still young, but it is already powering two extension types in production: product subscriptions and post-purchase extensions. These live in two different platform areas, which gives the team a useful signal about how well the technique generalizes.

The work ahead is substantial. The team plans to improve the third-party developer experience, add support for new UI patterns as they emerge, and open up more platform surfaces to this kind of extensibility.

Further reading

For a deeper look at the implementation, the remote-ui repository includes a comprehensive example that walks through the concepts end to end. There is also a React Summit talk covering remote rendering with web workers.

The work described here was a collaborative effort. Thanks to Chris Sauve, Elana Kopelevich, James Woo, and Trish Ta for their contributions to this project and to the original write-up. Joey Freund, a manager on the core extensibility team, authored this piece; his team focuses on tools that let developers shape the Shopify platform to fit each merchant’s needs.