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.