Back to lab
deepdivesprogramming

WebAssembly Is the Plugin System We Always Wanted

WebAssembly Is the Plugin System We Always Wanted

For forty years, “let users extend the app” meant picking your poison. Embed a scripting language and you’re stuck with one language, a slow interpreter, and a sandbox you pray holds. Load native plugins and one bad .dll takes down the whole process - and you’ve handed third parties the keys to your address space. Ship a microservice API and now every “plugin” is a network round-trip with its own deploy pipeline. Every option traded away something you didn’t want to give up: speed, safety, or language freedom. You got two out of three on a good day.

WebAssembly is the first technology that hands you all three at once, and that’s not hype - it’s the reason plugin systems across the industry are quietly being rebuilt on top of it. Figma runs your plugins in it. Shopify runs merchant logic in it. Envoy, Zellij, and a growing list of infrastructure tools load WASM extensions the way they used to load shared libraries. The pieces finally clicked into place, and the result is genuinely the extensibility model we’ve been trying to build since the first macro language. Let me convince you.

The Three-Way Trade Nobody Could Win

Think about what a plugin system actually has to do. It runs code its author has never seen, on a user’s machine or a company’s servers, and it has to be fast (or nobody ships features in it), safe (or a malicious plugin owns your process), and ideally language-agnostic (or you’ve told half your developers their skills don’t apply). Historically you could pick two:

Embedded scripting (Lua, JavaScript via an embedded engine) gives you safety and decent language ergonomics, but you’re locked to that one language and its interpreter speed, and the sandbox is only as good as the engine’s track record. Native plugins (.so, .dll, .dylib) give you blazing speed and any language that compiles to native, but zero isolation - a plugin shares all your memory and can crash or compromise the host at will. Out-of-process / RPC plugins give you isolation and language freedom, but every call crosses a process or network boundary, and operationally you’re now running a fleet.

WebAssembly’s pitch is that it collapses the triangle. A WASM module is fast - it’s a compiled bytecode that JITs to near-native speed. It’s safe - it runs in a sandbox with no ambient access to memory, the filesystem, or the network unless you explicitly hand it a capability. And it’s language-agnostic - Rust, Go, C, C++, Zig, TinyGo, even compiled-to-WASM scripting languages all produce the same kind of module. For the first time, the trade isn’t a trade.

Sandboxing You Get for Free

The safety story is the part that sounds too good, so it’s worth being concrete about why it holds. A WASM module runs in linear memory - a single contiguous array of bytes that the runtime owns. The module can only address bytes inside that array; every memory access is bounds-checked against it. There is no pointer it can forge to reach the host’s memory, because from inside the sandbox the host’s memory simply doesn’t exist in the address space. A plugin that tries to scribble outside its sandbox doesn’t corrupt you - it traps, and the runtime hands you a clean error.

Crucially, the sandbox is deny-by-default. A freshly instantiated module can’t open a file, make a network call, or read the clock. It can do arithmetic and call functions you explicitly gave it. This is the inverse of native plugins, where a module starts with the full power of your process and you hope it behaves. With WASM, capabilities are things you grant, one at a time, through imports you control:

// Host side (wasmtime): the plugin can ONLY call what we hand it.
let mut linker = Linker::new(&engine);

// Give the plugin a logging function - and nothing else.
linker.func_wrap("host", "log", |caller: Caller<'_, State>, ptr: i32, len: i32| {
    let msg = read_string_from_guest(&caller, ptr, len);
    println!("[plugin] {msg}");
})?;

// We did NOT add filesystem, network, or clock imports.
// So the plugin physically cannot touch them. That's the whole game.

This is the same insight Figma reached when they built their plugin system: they compiled a JavaScript interpreter to WASM and ran third-party plugin code inside that, so even a plugin trying its hardest can only call the specific canvas-manipulation functions Figma chose to expose. The blast radius of a hostile plugin is exactly the set of capabilities you handed it, and not one byte more. You stop auditing plugins for what they might do and start reasoning about what you allowed - a far smaller, far more tractable question.

“Any Language” Is the Underrated Superpower

The safety gets the headlines, but the language-agnostic part is what changes how teams actually work. Your plugin authors write in what they already know. A data scientist extends your pipeline in Rust; a web developer writes a rule in TinyGo; someone ports an existing C library and exposes it as a plugin without rewriting a line of logic. They all compile to the same .wasm artifact, and your host loads all of them identically. You are no longer in the business of forcing a language on your ecosystem.

This is precisely the bet Shopify made with Shopify Functions. Merchants and app developers customize backend logic - pricing rules, discounts, shipping calculations - by shipping a WASM module. Shopify runs it at the edge, sandboxed, in under five milliseconds, instead of round-tripping to a central server for every customization. Authors write in Rust, Zig, or TinyGo; Shopify just runs bytecode. The platform got customizability, the authors got language choice, and end users got speed, all from the same mechanism.

# The author's view is almost boringly normal - compile to a wasm target.
$ cargo build --target wasm32-wasip1 --release
$ ls target/wasm32-wasip1/release/*.wasm
   discount_logic.wasm        # this is the entire deliverable

That .wasm file is the whole plugin. No container, no language runtime to ship alongside it, no “works on my machine.” A few hundred kilobytes of portable bytecode that any compliant runtime can execute the same way on a server, a laptop, or an edge node.

The Component Model: From “Shared Bytes” to “Shared Meaning”

For a long time the rough edge of WASM plugins was the boundary. Classic WASM only speaks numbers - i32, i64, f32, f64. Want to pass a string? You’re manually writing bytes into linear memory, passing a pointer and a length, and agreeing by hand on the encoding on both sides. It works, but it’s the kind of glue code that’s tedious to write and easy to get subtly wrong, and every host/plugin pair reinvents it.

This is the problem the WebAssembly Component Model solves, and its arrival (alongside WASI 0.2, which shipped in early 2024 and has been maturing since) is why 2026 feels like the year WASM plugins went from “promising” to “default choice.” The Component Model adds a real type system across the boundary, described in a language called WIT (WebAssembly Interface Types). You declare the shape of your plugin interface - records, lists, strings, results, enums - and the tooling generates the marshalling glue for every language automatically.

// plugin.wit - the contract between host and plugin, language-neutral.
package siktec:filter@1.0.0;

interface image-filter {
    record pixel { r: u8, g: u8, b: u8 }

    // A plugin promises to implement this. The host promises to call it.
    apply: func(input: list<pixel>, strength: f32) -> list<pixel>;
}

world plugin {
    export image-filter;
}

A Rust author and a Go author both generate bindings from that one .wit file. Neither writes pointer-and-length glue; neither agrees on string encoding by hand. They each implement apply in idiomatic code, compile to a component, and the host calls it as if it were a normal typed function. The boundary went from “shared bytes you both have to interpret” to “shared meaning the toolchain enforces.” That’s the unlock that makes large, multi-language plugin ecosystems pleasant instead of heroic.

You Don’t Have to Build the Plumbing Yourself

If all of this sounds like a lot of runtime machinery to stand up, here’s the optimistic news: you mostly don’t write it. The ecosystem matured around a few excellent pieces.

Wasmtime (from the Bytecode Alliance) is the production-grade runtime - it was first to fully support the Component Model and WASI 0.2, and it’s what you reach for when you want control and raw performance. Extism sits one level up and is purpose-built for exactly this use case: it’s a framework for adding a plugin system to an existing app, with host SDKs for more than a dozen languages, so you can give your Python, Node, Ruby, or Go application a WASM plugin layer in an afternoon. The host side often looks like this:

// Extism host SDK - load a plugin, call a function, done.
import createPlugin from "@extism/extism";

const plugin = await createPlugin("./discount_logic.wasm", {
    useWasi: true,
    // Grant capabilities explicitly - here, one allowed host.
    allowedHosts: ["api.internal.example.com"],
});

const result = await plugin.call("apply_discount", JSON.stringify(cart));
console.log(result.text());

That’s a complete, sandboxed, language-agnostic plugin invocation. The plugin could be written in any of half a dozen languages; the host doesn’t know or care. The hard parts - memory management across the boundary, capability wiring, lifecycle - are handled. You’re left writing the part that’s actually about your product.

Where This Is Already Winning

The reason to be optimistic isn’t a roadmap, it’s a track record. The pattern is already load-bearing in production across wildly different domains:

  • Design tools - Figma sandboxes third-party plugins in WASM so they can reshape the canvas through a controlled API and nothing else, letting a huge plugin marketplace exist without trusting any single plugin.
  • E-commerce - Shopify Functions run merchant-authored backend logic at the edge in single-digit milliseconds, in any language that compiles to WASM.
  • Infrastructure - Envoy loads WASM filters for custom request processing, so you extend the proxy in Rust or Go without recompiling it or shipping a C++ extension into a critical path.
  • Developer tools - terminal multiplexers, editors, and CLIs (Zellij, Lapce, and friends) expose WASM plugin APIs so their communities can add features in safe, isolated modules.

Different industries, different languages, same architecture: a host that grants narrow capabilities, plugins compiled to portable sandboxed bytecode, a typed boundary between them. When a pattern shows up independently in design software, payment systems, and network proxies, it’s not a fad - it’s a primitive that was missing, and everyone reached for it the moment it existed.

The Honest Caveats (There Aren’t Many)

Optimism shouldn’t mean dishonesty, so: the tooling, while excellent, is still young in spots, and the Component Model’s ecosystem is racing toward the planned WASI 1.0 stabilization. Toolchains for some languages are more polished than others. Very chatty host/plugin interfaces can spend real time crossing the boundary, so you design for coarse-grained calls rather than thousands of tiny ones. And debugging across the WASM boundary is improving but isn’t yet as smooth as debugging a single native process.

None of these are foundational - they’re the rough edges of a young ecosystem rounding off quarter by quarter, not flaws in the model. The core proposition is solid and shipping: you can run untrusted, arbitrary-language code, fast, with a sandbox you can actually reason about, behind a typed interface that generates its own glue. That is the plugin system we’ve wanted since the first time someone asked “can users add their own features?” - and the honest answer, finally, is “yes, and safely, in whatever language they like.”

Build One

If you maintain anything that users want to extend - an app, a pipeline, a service, a tool - the experiment is cheap and the payoff is large. Define a .wit interface that captures one extension point. Pull in Extism or Wasmtime. Write one plugin in your favorite language, compile it to .wasm, and watch your host call it with a sandbox you didn’t have to invent. The first time a colleague writes a plugin in a language your codebase has never contained, and it just runs, safely, you’ll understand why the whole industry is quietly moving here. The plugin system we always wanted turned out to be a stack of bytecode, a capability list, and a type file - and it’s ready now.


Want a plugin layer in your product but not sure where the extension points should live? That’s the kind of architecture we love designing →

Sources:

Let's build

Build
better things.

Small team, full stack, real results. If you have an interesting engineering problem, we want in.