How to Debug Instatic Plugins in the QuickJS-WASM Sandbox: A Complete Guide
Instatic isolates every server-side plugin inside a QuickJS-WASM sandbox with strict resource limits, and you debug them using api.plugin.log(), console redirection, and VM error handling hooks that surface stack traces to the host server logs with the [plugin:<id>] prefix.
The CoreBunch/Instatic framework runs all server-side plugins inside a lightweight QuickJS-WASM sandbox to prevent runaway code from affecting the host process. Because this environment is isolated from the main Bun process by a dedicated worker, debugging requires specific hooks to capture logs and handle VM-level errors. Understanding how to debug Instatic plugins in the QuickJS-WASM sandbox allows you to trace execution, diagnose resource violations, and inspect failures without crashing the server.
How the QuickJS-WASM Sandbox Is Initialized
Worker and VM Creation
The sandbox lifecycle begins in server/plugins/pluginWorker.ts, which spawns a dedicated Bun worker for each plugin. This worker then invokes createPluginVm in server/plugins/quickjs/vm.ts to instantiate the QuickJS runtime. The factory creates a completely isolated VM context that persists for the plugin's lifetime, storing persistent handles for dispatcher functions after initial evaluation.
Resource Limits and Deadlines
Before any plugin code executes, the VM enforces strict budgets defined in server/plugins/quickjs/limits.ts:
DEFAULT_MEMORY_LIMIT_BYTES– Caps heap allocation per pluginDEFAULT_STACK_SIZE_BYTES– Limits stack depth to prevent infinite recursionevalTimeoutMs– Sets a wall-clock deadline (default 5 seconds) for the initial plugin bundle evaluation
If a plugin exceeds these boundaries, the VM triggers an interrupt that bubbles up to the host as a catchable error, allowing the server to log the violation and dispose of the VM safely.
Host Function Injection
The factory wires three critical host APIs into the sandbox's global scope:
__hostCall– Asynchronous RPC that returns a VM-sidePromisefor communicating with the host__hostSleep– Timer polyfill backing the bootstrap'ssetTimeoutandsetIntervalimplementations__log– Fire-and-forget logger that forwards string messages toargs.env.logon the host
These functions bridge the gap between the isolated WASM runtime and the host's logging infrastructure.
Bootstrap and Plugin Evaluation
Execution follows a two-stage process:
- Bootstrap evaluation – The typed bootstrap from
server/plugins/quickjs/bootstrap/generated/pluginBootstrap.tsruns first. It registers dispatcher functions like__runLifecycleand__runRoute, and redirects allconsole.*methods toapi.plugin.log. - Plugin evaluation – The plugin bundle (an IIFE that populates
globalThis.__plugin_exports) executes under the wall-clock deadline. After successful evaluation, the factory stores persistent handles for each dispatcher name inDISPATCHER_NAMESfor subsequent lifecycle calls.
Debugging Entry Points in the Sandbox
Structured Logging with api.plugin.log
The primary debugging mechanism is api.plugin.log(level, ...messages), which calls the injected __log function. This streams output directly to the host logger (args.env.log) with the format:
[plugin:<id>] [level] message content
Accepted levels are 'debug', 'info', 'warn', and 'error'.
Console Redirection
You can use standard console.log, console.error, and other console methods inside your plugin code. The bootstrap automatically rewires these to api.plugin.log, ensuring all console output appears in the server logs with the proper plugin prefix.
VM Error Handling
The VM's runtime.executePendingJobs() method pumps pending micro-tasks. When pumpPendingJobs catches an uncaught exception, it logs the stack trace with the prefix:
[plugin:<id>] VM job aborted: ...
This includes errors thrown explicitly in your plugin or runtime exceptions from malformed JavaScript.
Resource Violation Reporting
When a plugin hits memory limits, stack limits, or the evaluation timeout, the violation triggers an error that surfaces in the same format as VM job aborts. This allows you to identify infinite loops or memory leaks without debugging inside the WASM boundary.
Step-by-Step Debugging Workflow
-
Add logging statements inside your plugin source using either
api.plugin.log('debug', 'message')orconsole.log('message'). -
Run Instatic locally using
bun run dev. The development server prints all plugin messages to the console with the[plugin:your-plugin-id]prefix, making it easy to correlate output with specific plugins. -
Inspect VM-side errors when they occur. If your plugin throws an unhandled exception, the host logs the full stack trace. The VM always disposes safely (see the
dispose()implementation at the end ofvm.ts), so you can reload the plugin without restarting the server. -
Tune execution limits if you encounter timeouts during heavy initialization. Increase
evalTimeoutMswhen callingcreatePluginVm({ ..., evalTimeoutMs: 15000 })to extend the evaluation window from the default 5 seconds to 15 seconds. -
Capture logs programmatically by providing a custom
logimplementation inPluginVmEnvif you need to forward logs to a file, external service, or UI dashboard instead of stdout.
Practical Code Examples
// ---------------------------------------------------------------------------
// Example: Using the host logging API from inside a plugin
// ---------------------------------------------------------------------------
export function activate(api) {
// Direct logger – respects the level hierarchy set by the host.
api.plugin.log('debug', 'activate hook started')
// Console is automatically routed to the same logger.
console.info('plugin environment ready')
}
// ---------------------------------------------------------------------------
// Example: Throwing an error to see VM error handling
// ---------------------------------------------------------------------------
export function install(api) {
// This will be caught by the VM pump and printed as a VM job abort.
throw new Error('boom! simulate a failure')
}
// ---------------------------------------------------------------------------
// Example: Extending the VM timeout for a long-running schedule
// ---------------------------------------------------------------------------
import { schedule } from '@core/plugins';
// schedule with a custom timeout (ms)
schedule('my-schedule', { maxDurationMs: 30_000 }, async (api) => {
api.plugin.log('info', 'starting long work')
await doHeavyComputation()
api.plugin.log('info', 'finished')
});
Key Files for Debugging Reference
| File | Role |
|---|---|
server/plugins/quickjs/vm.ts |
VM factory that creates the sandbox, wires host functions, enforces limits, and provides the PluginVm API. |
server/plugins/quickjs/limits.ts |
Defines default memory, stack, and evaluation timeout budgets for each sandboxed plugin. |
server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts |
Typed bootstrap that defines dispatcher functions and redirects console.* to api.plugin.log. |
server/plugins/pluginWorker.ts |
Spawns the Bun worker that owns a QuickJS VM for each plugin. |
docs/features/plugin-system.md |
High-level overview of the plugin architecture and security model. |
Summary
- The VM factory in
server/plugins/quickjs/vm.tscreates isolated sandboxes with configurable resource limits defined inserver/plugins/quickjs/limits.ts. - Use
api.plugin.log()or standardconsole.*methods to stream debug output to the host console with the[plugin:<id>]prefix. - VM errors and resource violations surface as
[plugin:<id>] VM job abortedmessages containing stack traces. - Adjust
evalTimeoutMswhen creating the VM or usemaxDurationMsin schedules for long-running tasks. - The bootstrap in
server/plugins/quickjs/bootstrap/generated/pluginBootstrap.tshandles console redirection and dispatcher registration.
Frequently Asked Questions
How do I see console.log output from my Instatic plugin?
The bootstrap script automatically redirects all console.* methods to api.plugin.log, which forwards messages to the host logger. When you run bun run dev, you will see output prefixed with [plugin:<id>] in the server console. You can also call api.plugin.log('info', 'message') directly for the same result.
What happens if my plugin hits the 5-second execution limit?
If plugin evaluation exceeds the default evalTimeoutMs of 5 seconds, the QuickJS runtime interrupts execution and throws an error that bubbles up to the host. This appears in the logs as [plugin:<id>] VM job aborted: ... followed by a timeout message. You can increase this limit by passing a higher evalTimeoutMs value to createPluginVm in server/plugins/quickjs/vm.ts.
Can I capture plugin logs to a file instead of the console?
Yes. The __log function injected into the sandbox forwards to args.env.log, which is configurable. When setting up the plugin VM, provide a custom log implementation in the PluginVmEnv options that writes to a file or external service instead of stdout. This allows centralized logging for all sandboxed plugins.
Where are uncaught plugin errors logged?
Uncaught exceptions are caught by pumpPendingJobs inside the VM runtime and forwarded to the host with the plugin ID prefix. They appear in the server console as [plugin:<id>] VM job aborted: ... followed by the error message and stack trace. The VM is then safely disposed via the dispose() method, preventing the error from crashing the host worker.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →