# How Instatic Executes Plugin Entrypoints in Sandboxed QuickJS Contexts

> Learn how Instatic executes plugin entrypoints in sandboxed QuickJS contexts. Discover its secure approach to isolating plugin code with Bun Workers and eliminating API access.

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

---

**Instatic runs every plugin entrypoint inside an isolated QuickJS-WASM sandbox where a Bun Worker instantiates a fresh VM, evaluates a bootstrap runtime that exposes dispatcher functions, and routes all host interactions through a typed protocol, completely eliminating ambient access to Node.js or Bun APIs.**

The CoreBunch/Instatic repository implements a strict security model where third-party plugins operate in tightly-controlled environments. When you execute plugin entrypoints in sandboxed contexts, Instatic orchestrates a deterministic pipeline that loads the plugin bundle into a QuickJS VM, initializes a bootstrap runtime to register capability handlers, and dispatches lifecycle hooks through JSON-serialized messages mediated by the `__hostCall` function.

## The Worker-to-VM Pipeline

Instatic isolates plugin execution by delegating all VM operations to a dedicated Bun Worker thread, ensuring that even catastrophic plugin failures cannot crash the main server process.

### Spawning the Bun Worker

The entry point resides in [`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts), which listens for `load-plugin` requests from the main process. When the host needs to activate a plugin, it posts a message containing the `pluginId`, `entryFileUrl`, manifest, and settings. The worker maintains a `vmsByPluginId` Map to track active plugin contexts.

```typescript
// Main process initiates the load
await host.postMessage({
  kind: 'load-plugin',
  correlationId: 'abc123',
  pluginId: 'my-plugin',
  entryFileUrl: '/plugins/my-plugin/bundle.js',
  manifest: { version: '1.0.0', grantedPermissions: [] },
  settings: {}
});

```

### Wrapping and Loading the Bundle

Upon receiving a load request, the worker reads the plugin's bundled entrypoint from disk using `readFile` and transforms the ESM module into a global-compatible format via `wrapEsmAsGlobal`. This wrapper exposes the plugin's exports as `globalThis.__plugin_exports`, allowing the QuickJS runtime to access the module without native ESM support inside the VM.

```typescript
// From server/plugins/pluginWorker.ts
async function handleLoadPlugin(msg: LoadPluginRequest) {
  const rawSource = await readFile(msg.entryFileUrl, 'utf-8');
  const pluginSource = wrapEsmAsGlobal(rawSource, '__plugin_exports');
  
  const vm = await createPluginVm({
    pluginSource,
    env: {
      pluginId: msg.pluginId,
      manifestVersion: msg.manifest.version,
      grantedPermissions: msg.manifest.grantedPermissions ?? [],
      assetBasePath: `/uploads/plugins/${msg.pluginId}/${msg.manifest.version}`,
      settings: msg.settings,
      hostCall: (t, a) => callHostApi(msg.pluginId, t as any, a),
      log: (a) => send({ kind: 'log', pluginId: msg.pluginId, args: a })
    }
  });
  
  vmsByPluginId.set(msg.pluginId, vm);
  send({ kind: 'load-plugin-result', correlationId: msg.correlationId, ok: true, hooks: vm.exportedHooks });
}

```

## Bootstrap Runtime Initialization

After the worker loads the plugin source, Instatic evaluates a bootstrap script inside the VM that constructs the restricted API surface and registers dispatcher functions.

### Creating the QuickJS Context

The `createPluginVm` function (from [`quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/quickjs/vm.ts)) spins up a fresh QuickJS context with enforced memory and stack limits. This VM has **zero ambient access** to Bun or Node APIs; all interaction must flow through the `hostCall` callback provided during initialization.

### Installing the Bootstrap Runtime

The worker evaluates [`server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts), which executes the logic defined in [`server/plugins/quickjs/bootstrap/src/pluginRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/src/pluginRuntime.ts). This bootstrap:

- Registers `globalThis.__plugin_handlers` to store callbacks for routes, hooks, and filters
- Defines dispatcher functions including `__runLifecycle`, `__runRoute`, `__runHookListener`, and `__runHookFilter`
- Constructs the plugin API via `__buildApi()`, which exposes `api.cms.*` methods that register handlers in the global registry

## Entrypoint Resolution and Execution

Once the bootstrap completes, Instatic resolves the plugin's exported lifecycle object and executes registered hooks through the dispatcher layer.

### Resolving Plugin Exports

The `__resolvePluginModule()` function in [`pluginRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/pluginRuntime.ts) normalizes the module structure, supporting both named exports (a lifecycle object with `activate`, `deactivate` methods) and default exports. This allows the host to locate specific hooks like `activate` or `install` regardless of the plugin's export style.

```typescript
// From pluginRuntime.ts
globalThis.__resolvePluginModule = function resolvePluginModule() {
  const exports = globalThis.__plugin_exports;
  // Supports both: export const lifecycle = { activate: ... }
  // and: export default { activate: ... }
  return exports?.lifecycle || exports?.default || exports;
};

```

### Dispatching Lifecycle Hooks

The host triggers plugin code execution by posting `run-lifecycle` messages to the worker, which invokes the corresponding dispatcher inside the VM. The `__runLifecycle` function serializes arguments to JSON, calls the plugin's hook with the constructed API, and returns the result.

```typescript
// Host requests activation
await host.postMessage({
  kind: 'run-lifecycle',
  correlationId: 'def456',
  pluginId: 'my-plugin',
  hook: 'activate'
});

// Inside the VM (pluginRuntime.ts)
globalThis.__runLifecycle = async function runLifecycle(hook) {
  const mod = __resolvePluginModule();
  const fn = mod && mod[hook];
  if (typeof fn !== 'function') return;
  await fn(globalThis.__buildApi());
};

```

### Handling Route Requests

For HTTP routes registered via `api.cms.routes.register`, the host dispatches through `__runRoute`. This function deserializes the request context, executes the registered handler, and encodes the response for transmission back to the main process.

```typescript
globalThis.__runRoute = async function runRoute(routeKey, ctxJson) {
  const handler = globalThis.__plugin_handlers.routes[routeKey];
  if (!handler) throw new Error('Route handler not registered: ' + routeKey);
  
  const ctx = fromJson(ctxJson);
  const result = await handler({ req, body, user: ctx.user });
  
  if (result && typeof result === 'object' && result.__response === true) {
    const encoded = __encodeResponseBody(result.body);
    return toJson({
      __response: true,
      status: result.status ?? 200,
      headers: result.headers ?? {},
      body: encoded.body,
      bodyEncoding: encoded.bodyEncoding
    });
  }
  return toJson(result, { ok: true });
};

```

## Security Boundaries and Host Isolation

The sandbox design eliminates Remote Code Execution (RCE) risks by enforcing capability restrictions at multiple layers.

### Per-Plugin Resource Limits

Each QuickJS VM created by `createPluginVm` operates under configurable memory and stack constraints. If a plugin exceeds these limits, the VM termination is handled by the worker without affecting other plugins or the host process.

### The Host Call Protocol

All privileged operations (file system access, database queries, HTTP requests) route through `__hostCall`, which implements the typed protocol defined in [`server/plugins/protocol/messages.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/protocol/messages.ts). The worker validates permissions against the `grantedPermissions` array from the plugin manifest before executing any host-side API calls, ensuring that capabilities are explicitly granted rather than implicitly available.

## Summary

- **Worker Isolation**: [`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts) runs each plugin in a separate Bun Worker thread, preventing crashes from affecting the main server.
- **VM Creation**: `createPluginVm` instantiates a fresh QuickJS context with memory limits and no access to Node.js or Bun globals.
- **Bootstrap Initialization**: The runtime evaluates [`server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts) to install dispatcher functions and the `api.cms.*` handler registry.
- **Entrypoint Resolution**: `__resolvePluginModule()` normalizes exports to locate lifecycle hooks like `activate` and `deactivate`.
- **Message-Based Dispatch**: Host invokes plugin code via `__runLifecycle`, `__runRoute`, and other dispatchers that serialize arguments and responses as JSON.
- **Capability Security**: The `__hostCall` mechanism and [`server/plugins/protocol/messages.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/protocol/messages.ts) enforce per-plugin permission grants, blocking unauthorized system access.

## Frequently Asked Questions

### How does Instatic prevent plugins from accessing the file system directly?

Plugins execute inside a QuickJS VM that lacks any built-in file system APIs. The bootstrap runtime does not expose `fs` or `Bun.read` to the global scope. If a plugin requires file operations, it must invoke `api.cms.*` methods that translate to `__hostCall` messages, which the worker validates against the plugin's `grantedPermissions` manifest before executing the request on the host.

### What happens if a plugin exceeds its memory or stack limits?

The `createPluginVm` function applies per-context memory and stack constraints to the QuickJS runtime. If a plugin exceeds these limits during execution (such as during an infinite loop or memory leak), the QuickJS engine triggers an out-of-memory error that halts the specific VM. The worker captures this exception, terminates the plugin context, and reports the failure to the host without impacting other sandboxed plugins or the main Bun process.

### Can plugins import external npm modules within their sandboxed contexts?

No. The sandboxed QuickJS context does not have a module resolution system or access to `node_modules`. Plugins must be bundled as self-contained JavaScript files (processed via `wrapEsmAsGlobal`) that include all dependencies at build time. The VM evaluates the bundled string directly, and any dynamic import attempts will fail because the runtime lacks a loader for external packages.

### How are HTTP responses returned from sandboxed route handlers?

When a plugin registers a route handler via `api.cms.routes.register`, the callback is stored in `globalThis.__plugin_handlers.routes`. The host later invokes `__runRoute(routeKey, ctxJson)` inside the VM, which executes the handler and serializes the result. If the handler returns a response object marked with `__response: true`, the bootstrap encodes the body and headers into JSON via `toJson()`, transmits this back through the worker's `postMessage` channel, and the host reconstructs the data into a real HTTP Response object.