# How Instatic's Plugin System Uses QuickJS-WASM Sandboxing

> Discover how Instatic's plugin system leverages QuickJS-WASM sandboxing within Bun Workers for secure, isolated third-party plugin execution with strict resource limits and permission-checked host interactions.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-03

---

**Instatic executes every third-party plugin inside an isolated QuickJS-WASM context running within a Bun Worker, enforcing strict memory and stack limits while mediating all host interactions through permission-checked bridge functions.**

Instatic, an open-source CMS from the CoreBunch organization, implements a **deterministic, resource-bounded sandbox** for server-side plugin execution. The architecture guarantees that arbitrary JavaScript code cannot access the host filesystem, network, or memory outside its allocated bounds. This is achieved through a multi-layered isolation strategy combining the `quickjs-emscripten` WebAssembly interpreter, Bun's Worker threads, and a tightly controlled API façade.

## Sandboxing Architecture Overview

The execution topology creates three distinct isolation boundaries:

```

Bun host (main process)
 └─ Bun.Worker (crash-isolation, CPU-yield)
      └─ QuickJS-WASM context (security sandbox)
           ├─ Bootstrap (SDK façade + handler registries)
           └─ Plugin source (IIFE → globalThis.__plugin_exports)

```

**Context isolation** ensures each plugin receives its own QuickJS virtual machine. The host never shares JavaScript objects with the VM; instead, communication occurs exclusively through **host functions** registered within the VM (`__hostCall`, `__hostSleep`, `__log`). This prevents prototype pollution or memory leakage between the host and guest environments.

## Resource Constraints and Memory Limits

Before any user code executes, the runtime enforces hard caps on consumption. In [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts), the `createPluginVm` factory applies two critical thresholds:

- **Memory limit**: `DEFAULT_MEMORY_LIMIT_BYTES` caps the heap size
- **Stack limit**: `DEFAULT_STACK_SIZE_BYTES` restricts call depth

These limits are defined in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) and apply per-plugin. If a script exceeds either boundary, QuickJS raises a catchable exception that the host converts into a sandbox violation error, terminating the specific plugin without affecting the worker thread or other plugins.

## Bootstrap Process and Dispatcher Registration

Every QuickJS context undergoes a controlled initialization sequence. First, the VM evaluates the `BOOTSTRAP_SOURCE` string exported from [`server/plugins/quickjs/bootstrap/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/index.ts). This bootstrap script:

1. Installs polyfills for timers (`setTimeout`, `clearTimeout`)
2. Registers **dispatcher functions** (`__runLifecycle`, `__runRoute`, etc.) that act as the plugin's public API surface
3. Prepares `globalThis.__plugin_exports` to receive the plugin's IIFE bundle

After the bootstrap completes, the actual plugin code—compiled as an IIFE that writes to `globalThis.__plugin_exports`—is evaluated. The host stores persistent handles to these dispatchers in a `dispatcherHandles` map, keyed by the `DISPATCHER_NAMES` constant defined in [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts).

## Host-to-Plugin Invocation

When the CMS needs to execute plugin logic, it invokes strongly-typed methods on the `PluginVm` handle returned by `createPluginVm`. These methods—`runLifecycle`, `runRoute`, `runHookListener`, and `runSchedule`—forward to generic helpers in [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts) (`callString`, `callVoid`, `evalJson`) that:

- Look up the dispatcher handle by name
- Marshal arguments using [`server/plugins/quickjs/marshal.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/marshal.ts)
- Enforce execution deadlines
- Return deserialized JSON results

This architecture keeps the host in control of when and how plugin code runs, preventing guest scripts from blocking the event loop.

## Plugin-to-Host Communication and Permissions

Plugins cannot directly import Node.js modules or access Bun APIs. Instead, they interact with the CMS through `env.hostCall`, which implements the **plugin SDK API** (`api.plugin.*`, `api.cms.*`, etc.).

In [`server/host/apiDispatch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/host/apiDispatch.ts), the `dispatchApiCall` function validates the plugin's granted permissions against the requested operation before executing it. For example, a plugin with only `cms.read` permission that attempts to write will be rejected at the host boundary. All data crossing this boundary must be JSON-serializable, ensuring no object references leak between contexts.

## Micro-Task Management and Event Loop

QuickJS queues promise continuations and timer callbacks inside its own internal micro-task queue. After every host-side promise resolves, the runtime explicitly drains pending jobs via `runtime.executePendingJobs()` (wrapped in `pumpPendingJobs` inside [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts)). This guarantees that asynchronous plugin code completes promptly and never stalls the Bun Worker thread.

## Graceful Shutdown and Resource Cleanup

When a plugin is unloaded or reloaded, the `dispose()` method in [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts) executes a deterministic teardown sequence:

1. Marks the VM as dead to prevent new calls
2. Clears all pending timers
3. Disposes any pending `Deferred` promises
4. Releases all host-function handles
5. Destroys the QuickJS context

This strict disposal order prevents use-after-free crashes and ensures the Bun Worker can be safely terminated or reused.

## Practical Example

The following demonstrates creating, executing, and safely destroying a sandboxed plugin VM:

```typescript
import { createPluginVm, type PluginVmEnv } '@/server/plugins/quickjs/vm';

// 1. Build the host environment that the VM sees
const env: PluginVmEnv = {
  pluginId: 'my-awesome-plugin',
  manifestVersion: '1.0.3',
  grantedPermissions: ['cms.read', 'cms.write'],
  assetBasePath: '/uploads/plugins/my-awesome-plugin/1.0.3',
  settings: { theme: 'light' },

  // Host-side implementation of the SDK API
  async hostCall(target, args) {
    // Validate permission and dispatch to the real handler
    return await dispatchApiCall(target, args);
  },

  // Simple fire-and-forget logger
  log(args) {
    console.log('[plugin:my-awesome-plugin]', ...args);
  },
};

// 2. Load the compiled plugin bundle (generated by the SDK)
const pluginSource = await Bun.file('./plugins/my-awesome-plugin/dist/plugin.js').text();

// 3. Spin up a sandboxed VM
const vm = await createPluginVm({ pluginSource, env });

// 4. Run lifecycle hooks (install, activate, etc.)
await vm.runLifecycle('install');

// 5. Execute a custom route defined by the plugin
const result = await vm.runRoute('myRoute', {
  request: { url: '/api/foo', method: 'GET', headers: {}, body: '', bodyEncoding: 'utf8' },
  body: {},
  user: null,
});
console.log('Route result →', result);

// 6. Clean up when the plugin is removed or reloaded
vm.dispose(); // Releases all QuickJS resources safely

```

## Summary

Instatic's **QuickJS-WASM sandboxing** strategy delivers secure, server-side plugin execution through several key mechanisms:

- **Process isolation**: Each plugin runs in a dedicated Bun Worker containing a separate QuickJS context
- **Resource enforcement**: Hard memory and stack limits defined in [`limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/limits.ts) prevent resource exhaustion
- **Controlled API surface**: All host interactions flow through `hostCall` with permission validation in [`apiDispatch.ts`](https://github.com/CoreBunch/Instatic/blob/main/apiDispatch.ts)
- **Deterministic lifecycle**: Bootstrap dispatchers in [`bootstrap/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/bootstrap/index.ts) and explicit cleanup via `dispose()` eliminate resource leaks
- **Async safety**: Micro-task draining via `pumpPendingJobs` keeps the event loop responsive

## Frequently Asked Questions

### How does Instatic prevent plugins from accessing the host filesystem?

Instatic **never shares host objects** with the QuickJS VM. The WASM interpreter has no access to Node.js or Bun built-in modules. File system operations must traverse the `hostCall` bridge to [`server/host/apiDispatch.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/host/apiDispatch.ts), where the host validates permissions before executing any I/O on the plugin's behalf.

### What happens when a plugin exceeds its memory limit?

If a plugin's heap allocation exceeds `DEFAULT_MEMORY_LIMIT_BYTES` or its stack exceeds `DEFAULT_STACK_SIZE_BYTES`, QuickJS throws a catchable exception. The host catches this in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) and terminates the specific plugin, returning an error to the caller without crashing the Bun Worker or affecting other plugins.

### How does the plugin SDK communicate with the host?

The compiled plugin bundle runs inside an IIFE that populates `globalThis.__plugin_exports`. During bootstrap, the VM registers dispatcher functions that the host later invokes via `callString` and `callVoid` helpers in [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts). When the plugin needs host services, it calls `env.hostCall`, which serializes the request, exits the VM, and lets the host validate permissions before executing the operation.

### Is the QuickJS sandbox reusable after a plugin stops?

No. Once `vm.dispose()` is called, the QuickJS context is permanently destroyed and its memory freed. The `createPluginVm` factory in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) creates fresh contexts for each plugin load, ensuring no state persists between executions and preventing cross-plugin contamination.