QuickJS Plugins

The QuickJS tier is the default plugin sandbox. It embeds the QuickJS JavaScript engine via rquickjs, giving each plugin its own isolated runtime with strict resource limits and no direct access to the filesystem, network, or host process.

Runtime Characteristics

Property Default Configurable
Memory limit 64 MB sandbox.max_memory_mb
Stack size 1 MB Fixed
Wall-clock timeout 10 seconds per call sandbox.max_time_secs
Startup time < 1 ms N/A
Per-instance overhead < 1 MB N/A

The runtime enforces these limits via QuickJS's built-in memory allocator hooks and an interrupt handler that checks wall-clock time after every N bytecode instructions. If a plugin exceeds its memory or time budget, the call returns an error and the plugin is not terminated -- it can still handle subsequent requests.

Available Globals

QuickJS plugins run in a restricted environment. The following globals are available:

console

Standard structured logging routed to the bext tracing pipeline:

console.log("info message");       // level=INFO
console.info("info message");      // level=INFO
console.warn("warning message");   // level=WARN
console.error("error message");    // level=ERROR
console.debug("debug message");    // level=DEBUG (only with BEXT_PLUGIN_LOG=debug)

bext.storage

Per-plugin scoped key-value storage backed by files on disk. Keys are sanitized to prevent path traversal (no .., /, \, or null bytes).

// Write a value (quota enforced via sandbox.storage_quota_kb)
bext.storage.set("last_seen", Date.now().toString());

// Read a value (returns null if not found)
const val = bext.storage.get("last_seen");

// Delete a value
bext.storage.delete("last_seen");

Storage is persisted across server restarts. Each plugin's data is isolated in its own directory under data/plugins/<plugin-name>/.

bext.fetch

Rate-limited, URL-allowlisted HTTP client. Only URLs matching the permissions.allowed_urls globs are permitted. An empty allowlist means no HTTP access at all.

// plugin.toml: permissions.allowed_urls = ["https://api.example.com/*"]

const response = bext.fetch("https://api.example.com/v1/data", {
  method: "POST",
  headers: [["Content-Type", "application/json"]],
  body: JSON.stringify({ key: "value" }),
});

console.log(response.status);  // 200
console.log(response.body);    // response body as string

The fetch rate is capped by permissions.max_fetch_per_minute (default: 30). Exceeding the limit returns an error.

bext.config

Read-only access to the plugin's configuration from bext.config.toml:

// bext.config.toml:
// [plugins.my-plugin.config]
// threshold = 100
// regions = ["us", "eu"]

const threshold = bext.config.threshold;  // 100
const regions = bext.config.regions;      // ["us", "eu"]

bext.metric

Emit custom metrics to the bext observability pipeline (Prometheus, OpenTelemetry):

bext.metric("requests_blocked", 1, [
  ["reason", "geo"],
  ["country", "CN"],
]);

What is NOT Available

The QuickJS sandbox deliberately excludes:

- require() and Node.js built-in modules

- globalThis.fetch (use bext.fetch instead)

- setTimeout, setInterval, queueMicrotask

- Deno, Bun, process globals

- File system access (fs, path)

- eval() and Function() constructor (code generation disabled)

- WebAssembly (use the WASM tier instead)

Complete Example: Geo-Redirect Plugin

This plugin redirects users to a region-specific subdomain based on a header set by your CDN:

// src/index.js
const REGION_MAP = {};

export function onServerStart(config) {
  // Build region -> URL map from config
  for (const [region, url] of Object.entries(config.regions)) {
    REGION_MAP[region] = url;
  }
  console.log(`Geo-redirect loaded with ${Object.keys(REGION_MAP).length} regions`);
}

export function onRequest(ctx) {
  // Read region from CDN header
  const regionHeader = ctx.headers.find(([k]) => k === "x-geo-region");
  if (!regionHeader) {
    return { action: "continue" };
  }

  const region = regionHeader[1].toLowerCase();
  const redirectUrl = REGION_MAP[region];

  if (!redirectUrl) {
    return { action: "continue" };
  }

  // Don't redirect API calls or static assets
  if (ctx.path.startsWith("/api/") || ctx.path.startsWith("/_next/")) {
    return { action: "continue" };
  }

  // Check if user already dismissed the redirect
  const dismissed = bext.storage.get(`dismissed:${ctx.peer_ip}`);
  if (dismissed === "true") {
    return { action: "continue" };
  }

  return {
    action: "respond",
    status: 302,
    headers: [
      ["Location", redirectUrl + ctx.path],
      ["Cache-Control", "private, no-cache"],
    ],
    body: "",
  };
}

export function onResponse(ctx, headers) {
  // Add Vary header so CDN caches per-region
  headers.headers.push(["Vary", "X-Geo-Region"]);
}

Configuration in bext.config.toml:

[plugins.geo-redirect]
sandbox = "quickjs"
path = "./plugins/geo-redirect/src/index.js"

[plugins.geo-redirect.config.regions]
eu = "https://eu.example.com"
ap = "https://ap.example.com"
us = "https://us.example.com"

Performance Considerations

QuickJS is an interpreter, not a JIT. For hot-path middleware that runs on every request, keep your onRequest handler lean:

- Pre-compute lookup tables in onServerStart rather than rebuilding them per request.

- Use bext.storage sparingly in the request path -- each call involves a disk read.

- For CPU-intensive work (cryptography, large JSON parsing), consider the WASM tier instead.

Typical onRequest overhead is 50-200 microseconds depending on handler complexity.