# QuickJS-WASM Plugin Sandbox Architecture in Instatic

> Explore the QuickJS-WASM plugin sandbox architecture in Instatic. Learn how Bun Workers, synchronous VMs, and RPC bridges ensure secure, high-performance plugin isolation.

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

---

**Instatic isolates every server-side plugin inside a multi-layered QuickJS-WASM sandbox that uses Bun Workers for crash isolation, synchronous QuickJS VMs for API-less execution, and permission-gated RPC bridges to enforce security without sacrificing performance.**

The QuickJS-WASM plugin sandbox architecture in CoreBunch/Instatic provides a hardened execution environment for untrusted JavaScript code running on the server. By combining WebAssembly-based virtualization with process-level isolation, the system ensures that plugins cannot access Node.js or Bun host APIs, exhaust resources, or crash the main process. This architecture centers on a synchronous QuickJS VM instantiated inside a dedicated Worker, communicating with the host through a strictly controlled set of bridged functions.

## The Five-Layer Isolation Model

Instatic implements defense in depth through a hierarchy of isolated layers, each preventing specific classes of failures from propagating.

- **Bun Host Process**: The main Instatic server that orchestrates plugin lifecycles and manages Worker pools.
- **Bun Worker**: Each plugin loads in its own `Worker` defined in [`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts), providing a separate event loop and crash containment.
- **QuickJS-WASM Context**: Inside the worker, [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) instantiates a synchronous QuickJS VM where no Node or Bun globals exist.
- **Bootstrap SDK**: [`server/plugins/quickjs/bootstrap/src/buildApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/src/buildApi.ts) evaluates first to register the SDK façade, dispatcher functions (`__runLifecycle`, `__runRoute`), and the permission map (`TARGET_PERMISSIONS`). The [`boundary.ts`](https://github.com/CoreBunch/Instatic/blob/main/boundary.ts) file in the same directory validates RPC payloads crossing this layer.
- **Plugin Bundle**: The plugin code—an IIFE that populates `globalThis.__plugin_exports`—runs atop the bootstrap, exposing only the hooks defined by the SDK.

## Resource Limits and Execution Deadlines

Security hinges on硬性 constraints applied before any untrusted code executes.

The [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) file defines default caps for memory, maximum stack size, and evaluation timeout. The VM factory applies these limits during initialization in [`vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/vm.ts) by configuring the QuickJS runtime.

Every entry into the VM—including bootstrap evaluation, plugin loading, dispatcher calls, and timer callbacks—is wrapped with `withSyncDeadline` from [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts). If the wall-clock deadline expires, the VM forcibly aborts the current job, ensuring that infinite loops cannot hang the Worker. This deadline guard also wraps `runtime.executePendingJobs` when the host pumps the micro-task queue after resolving promises or firing timers.

## Permission Enforcement and RPC Bridging

Communication between the sandbox and host uses a synchronous host-function bridge that returns QuickJS `Promise` objects resolved asynchronously by the host.

The VM registers three core host functions in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts):
- `__hostCall`: Forwards RPC calls to the host API
- `__hostSleep`: Handles asynchronous delays
- `__log`: Captures console output

When the bootstrap runs, it creates the `TARGET_PERMISSIONS` map in [`server/plugins/protocol/targets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/protocol/targets.ts), associating each RPC target with required permission strings. Every invocation through `__hostCall` checks the plugin's `grantedPermissions` set against this map, throwing synchronously if unauthorized.

After bootstrap completion, the host stores persistent handles to dispatcher functions in `dispatcherHandles`. To invoke a plugin hook, the host retrieves the appropriate handle (e.g., `__runLifecycle`) and calls it via `ctx.callFunction`, passing JSON string payloads. The [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts) module provides typed helpers—`callString`, `callVoid`, and `evalJson`—to handle these crossings efficiently without re-parsing on the VM side.

## Timer Management and Memory Safety

The VM virtualizes `setTimeout` and `setInterval` by tracking handles in a `pendingTimers` map inside [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts). When the VM disposes, the host iterates this map to clear all timers, preventing callbacks into a destroyed context.

Between asynchronous events, the host drains the VM's micro-task queue using `runtime.executePendingJobs` under the deadline guard. This ensures that malicious micro-tasks or deeply recursive promises cannot block the Worker indefinitely.

## Canvas Module Variant

For canvas module packs, Instatic uses an identical architecture but substitutes [`server/plugins/modulePackVm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/modulePackVm.ts) for the standard VM factory. This variant uses `MODULE_PACK_BOOTSTRAP_SOURCE` instead of the standard plugin bootstrap, tailored for module-specific APIs while maintaining the same security boundaries.

## Implementation Example

The following TypeScript demonstrates creating a sandboxed plugin VM and invoking lifecycle hooks through the dispatcher:

```typescript
// Initialize the VM with resource limits and granted permissions
import { createPluginVm } from '@/server/plugins/quickjs/vm';
import type { PluginVmEnv } from '@/server/plugins/quickjs/types';

const env: PluginVmEnv = {
  pluginId: 'my-plugin',
  grantedPermissions: new Set(['cms.content.read', 'network.fetch']),
};

const pluginVm = await createPluginVm({
  pluginSource: `<bundle-IIFE>`, // Output from the plugin-SDK build pipeline
  env,
  evalTimeoutMs: 5000,
});

// Invoke the 'activate' hook via the bootstrap dispatcher
const resultJson = await pluginVm.__runLifecycle(
  JSON.stringify({ hook: 'activate', args: [] })
);
const result = JSON.parse(resultJson);

```

The host-side implementation of `__hostCall` serializes responses to JSON, which the VM receives as a resolved Promise:

```typescript
// Simplified host bridge using eval.ts helpers
import { callString } from '@/server/plugins/quickjs/eval';

async function __hostCall(target: string, args: unknown[]) {
  // Resolve against host API (e.g., cms.content.entries.create)
  const response = await resolveHostApi(target, ...args);
  return JSON.stringify(response); // Returned to VM as resolved Promise
}

```

## Summary

- **Process Isolation**: Each plugin runs in a dedicated Bun Worker ([`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts)) to contain crashes and prevent event-loop pollution.
- **API-less Execution**: The QuickJS-WASM VM ([`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts)) exposes no Node.js or Bun globals, presenting only the typed SDK façade generated by [`buildApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/buildApi.ts).
- **Resource Guards**: Hard limits on memory, stack, and execution time are enforced via [`limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/limits.ts) and `withSyncDeadline` in [`eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/eval.ts).
- **Permission Gating**: Every RPC validates against `TARGET_PERMISSIONS` in [`server/plugins/protocol/targets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/protocol/targets.ts), ensuring plugins access only explicitly granted capabilities.
- **Clean Lifecycle**: Timer tracking and micro-task pumping prevent resource leaks and ensure controlled shutdown without dangling callbacks.

## Frequently Asked Questions

### How does Instatic prevent plugins from accessing the filesystem or network directly?

The QuickJS-WASM VM runs without Node.js or Bun bindings, so standard APIs like `fs` or `fetch` are undefined. All privileged access flows through the bridged `__hostCall` function, which validates permissions against [`server/plugins/protocol/targets.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/protocol/targets.ts) before executing any host-side operations.

### What happens if a plugin enters an infinite loop?

Every VM entry point is wrapped with `withSyncDeadline` from [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts). When the configured wall-clock timeout expires, the VM forcibly aborts the current execution context, returning control to the host Worker and preventing the plugin from hanging the system.

### Can plugins use `setTimeout` and `setInterval`?

Yes, but these are virtualized. The bootstrap registers polyfills that track handles in `pendingTimers`. The host manages the actual timer queue and invokes callbacks back into the VM, checking deadlines after each batch of micro-tasks to maintain isolation.

### How are permissions configured for each plugin?

When creating the VM via `createPluginVm` in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts), the host passes a `grantedPermissions` Set in the `PluginVmEnv` configuration. The bootstrap code in [`server/plugins/quickjs/bootstrap/src/buildApi.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/src/buildApi.ts) compares these grants against the `TARGET_PERMISSIONS` map before allowing any RPC invocation to proceed.