A platform like Shopify is only as useful as the use cases it can cater to. That means customizability and extendibility are mission critical and are core principles for every project that is developer-facing. Merchants get a great experience out of the box when they sign up with Shopify, but their need for customization and personalization naturally increases over time as their business grows. For cases where a merchant and their developers want to take full control over every detail of their store, we offer the Storefront API. With Hydrogen and Remix, we’re helping developers move faster through their custom journey by setting them up with a well-lit path with baked-in best practices and tooling. Long story short: Developers can customize the front end. With Metafields, we allow developers to store custom data in our database. This is the second piece in the trifecta of customization: Developers can customize the database.
Shopify Functions are the last puzzle piece to complete the trifecta: Developers can customize the back end. Shopify Functions provide a mechanism for developers to inject their custom code and run it on our servers. The long-term goal is to make every part of a store’s pipeline replaceable with custom code, giving Shopify’s platform unprecedented flexibility without sacrificing security or scalability.
WebAssembly (or Wasm for short) is the key technology here. It’s a perfect fit, as it’s designed around flexibility, security and performance. The strong sandbox of Wasm lets us run untrusted code with confidence, its predictable performance lets us define and impose strict resource limits, and the rich and ever-growing ecosystem of languages that can target Wasm gives developers free choice on how they want to write their custom code.
At the Summer Editions 2022, we launched Shopify Functions with Rust being our recommended language. Not only has Shopify embraced Rust as its default systems programming language, Rust has great support for Wasm and has spawned a rich ecosystem of tooling specifically around Wasm.
Every function that runs on the Shopify Functions infrastructure is nothing less and nothing more than a WASI (WebAssembly System Interface) module. No smoke and mirrors or other special technologies are in use.
Wasm modules by themselves can only do arithmetic. They are completely sandboxed and only get an isolated chunk of memory to work with for their computations. The host system can expose individual functions to the Wasm module, granting granular access to additional capabilities.
WASI is a standardized collection of those functions and capabilities, with the goal to make Wasm useful outside the browser. If you want to know more about WASI, this blog post by Lin Clark from the BytecodeAlliance, of which Shopify is also a member, is a great introduction. The only part of WASI we rely on is the ability to read from stdin and write to stdout (and, of course, stderr).
There are some additional constraints that every module must fulfill, amongst other things to ensure that we can scale functions appropriately, even on Black Friday:
- The module must not be larger than 256KB.
- The module must not run longer than 5ms.
- The module must consume a JSON-formatted string via stdin and produce a JSON-formatted string on stdout.
Note: 5ms is a very machine-dependent and situational constraint. The same machine will need different amounts of time to execute the exact same function, depending on how the load the machine is under. We’re exploring a gas-like approach as a machine- and situation-independent measurement to give developers confidence that their function is fast enough.
However, at the time of writing, WebAssembly’s architecture makes JIT-ing impossible. There’s no way for Wasm to generate and then execute code, as the memory storing the instructions is completely inaccessible to the module itself. This is by design: By preventing any code that wasn’t present during module instantiation from being executed, almost all remote code execution vulnerabilities become impossible.
Javy has a somewhat unorthodox build setup with two stages. In the first stage, it will compile a small Rust program to a Wasm module using the standard Rust compiler. This program uses the QuickJS crates above to create an instance of the QuickJS engine inside the Wasm module, and hooks up stdin and stdout (more on that later). In the second stage, the build process will compile the actual Javy CLI, which will bundle the Wasm binary with the executable.
If we were building a native binary, we could just put the engine in a shared library and create a dynamically linked executable, drastically reducing the file size of the executable itself and only having to keep a single copy of the dynamic library in memory. Wasm doesn’t have support for dynamic linking yet, but it’s an active area of research and standardization. The Component Model proposal is looking likely to advance through the stages and paints a promising future as Luke Wagner shows in this presentation. Until the Component Model becomes reality and is supported in runtimes like Wasmtime, we had to come up with a different solution for dynamic linking that also doesn’t prevent us from pivoting to the Component Model later on.
Shopify’s Jeff Charles did a lot of work here and utilized Wasmtime’s linker, which connects and resolves the imported items of a Wasm module with the exported items provided by the host system or other modules. This functionality of the linker is also exposed to the Wasmtime CLI for example via the
--preload flag. With this in mind, we designed a minimal interface for our dynamic library “javy_quickjs_provider_v1”, that consists of only two functions:
realloc(old_ptr, old_size, alignment, new_size): Used to allocate, resize or free memory allocations.
eval_bytecode(ptr, size): runs the given bytecode in a fresh QuickJS instance.
We changed the small program from earlier to fit the new interface requirements. By default, Javy will continue to emit “statically linked” binaries, generated as described above. Those modules run out of the box with Wasmtime. Dynamic linking can be enabled using the
eval_bytecode with the given bytecode that has been embedded as well:
We also embed the original source code in a Wasm custom section (a section that doesn’t affect execution whatsoever). We do this so we can know which of the Javy runtime APIs are in use.
The WAT code above compiles to a Wasm module of a whopping 220 bytes plus the size of the bytecode—a drastic reduction in file size. To create and run a dynamically linked binary, you can use Javy as follows:
This means that we can not only share the provider amongst many Shopify Functions, we can also aggressively precompile and optimize it ahead of time. We can even deploy bug fixes and optimizations to the engine without developers having to redeploy their function, as long as there are no breaking changes to the interface or the semantics of the bytecode.
Javy global (inspired by the
Deno global of Deno), which will contain all of our Javy-specific, non-standard APIs. As of now, we provide two low-level functions here that closely resemble POSIX API calls:
These functions are intentionally very low-level to be flexible, but this makes them inconvenient to use. To give developers more convenient and intuitive APIs, we published the
javy library on npm which provides higher-level functions like
Similarly, we haven’t enabled the event loop in the QuickJS instance that Javy uses. That means that async/await, Promises, and functions like
setTimeout are syntactically available but never trigger their callbacks. We want to enable this functionality in Javy, but have to clear up a couple of open questions, like if and how to integrate the event loop with the host system or how to implement
setTimeout() from inside a WASI environment.
After trying to write a couple of small Shopify Functions with Javy directly, we realized a couple of easy wins. Every function has the same boilerplate: We start by reading bytes from stdin until we reach the end of the stream, turning the sequence of bytes into a string, and parsing that string as JSON. Then we run the actual business logic of the function. Afterwards, the result of the business logic has to be turned back into a JSON-formatted string, the string converted into a byte stream, and the byte stream piped back to stdout. This boilerplate should not have to be written by developers over and over.
To implement these changes, we had to write a little layer between Javy and the user code that facilitates both the boilerplate and that inversion of control (in other words, the developer’s function gets called instead of the developer calling other functions). This layer is published to npm under
@shopify/shopify_function, and the core logic is simple but effective:
user-function point to the developer’s code. As a nice side effect, the developer is also able to use libraries from npm like they are used to (for example, i18n) if they so desire. You don’t have to set any of this up yourself, either, as we wrote a template that you can use through the Shopify CLI (details below!).
GraphQL & TypeScript
You can find the source code of your newly created Shopify Function extension in
extensions/<extension name>/src/main.ts (or
To compile this function to Wasm, run
npm run build in the extension’s folder which will output the Wasm module to
dist/function.wasm. You can run that module locally using Wasmtime, our function-runner or through the Shopify CLI:
Performance & The Future
Mozilla’s SpiderMonkey & WebAssembly
Local Developer Preview
This post was written by Surma. DX at Shopify. Web Platform Advocate. Craving simplicity, finding it nowhere. He/him. Internetrovert 🏳️🌈
Find him on Twitter, GitHub, or at surma.dev.
We all get shit done, ship fast, and learn. We operate on low process and high trust, and trade on impact. You have to care deeply about what you’re doing, and commit to continuously developing your craft, to keep pace here. If you’re seeking hypergrowth, can solve complex problems, and can thrive on change (and a bit of chaos), you’ve found the right place. Visit our Engineering career page to find your role.