Building a Plugin
This guide walks you through creating a bext plugin from scratch. We will build a "maintenance-mode" middleware plugin that returns a 503 page when a flag is set, using the QuickJS sandbox.
Scaffold the Project
bext plugin init maintenance-mode
cd maintenance-mode
This creates the following structure:
maintenance-mode/
plugin.toml # Plugin manifest
src/
index.js # Plugin entry point
test/
plugin.test.js # Test harness
The Manifest: plugin.toml
Every plugin requires a plugin.toml that declares its identity and capabilities:
[manifest]
name = "maintenance-mode"
version = "0.1.0"
description = "Return a 503 maintenance page when enabled via config"
capabilities = ["middleware"]
[sandbox]
type = "quickjs"
max_memory_mb = 16
max_time_secs = 5
storage_quota_kb = 256
[permissions]
allowed_urls = []
max_fetch_per_minute = 0
Fields:
- manifest.name — unique identifier, lowercase with hyphens.
- manifest.capabilities — one or more of: middleware, transform, lifecycle, cache_backend.
- sandbox.type — quickjs, wasm, or nsjail.
- sandbox.max_memory_mb — memory limit for the sandbox (default: 64 MB).
- sandbox.max_time_secs — wall-clock timeout per hook call (default: 10s).
- permissions.allowed_urls — glob patterns for outbound HTTP. Empty means no HTTP access.
Writing Hook Functions
Open src/index.js. The plugin exports lifecycle hook functions:
// src/index.js
// Called once when the server starts
export function onServerStart(config) {
bext.storage.set("enabled", config.enabled ? "true" : "false");
console.log(`Maintenance mode: ${config.enabled ? "ON" : "OFF"}`);
}
// Called on every incoming request (middleware capability)
export function onRequest(ctx) {
const enabled = bext.storage.get("enabled");
if (enabled !== "true") {
return { action: "continue" };
}
// Allow health checks through
if (ctx.path === "/healthz" || ctx.path === "/readyz") {
return { action: "continue" };
}
return {
action: "respond",
status: 503,
headers: [
["Content-Type", "text/html"],
["Retry-After", "300"],
],
body: `<!DOCTYPE html>
<html><head><title>Maintenance</title></head>
<body><h1>We'll be back shortly</h1>
<p>Scheduled maintenance in progress.</p>
</body></html>`,
};
}
// Called after the response is sent (async, non-blocking)
export function onRequestComplete(event) {
if (event.status === 503) {
bext.metric("maintenance_blocked", 1, [
["path", event.path],
]);
}
}
// Called during graceful shutdown
export function cleanup() {
console.log("Maintenance mode plugin shutting down");
}
Available APIs
Inside the QuickJS sandbox, your plugin has access to:
| API | Description |
|---|---|
bext.storage.get(key) |
Read from per-plugin scoped KV store |
bext.storage.set(key, value) |
Write to KV store (quota enforced) |
bext.storage.delete(key) |
Delete a KV entry |
bext.fetch(url, options) |
Rate-limited, URL-allowlisted HTTP client |
bext.config |
Read-only plugin configuration object |
bext.metric(name, value, tags) |
Emit a metric to the observability pipeline |
console.log/warn/error/info/debug |
Structured logging |
Configuring the Plugin
In your app's bext.config.toml, add the plugin section:
[plugins.maintenance-mode]
sandbox = "quickjs"
path = "./plugins/maintenance-mode/src/index.js"
[plugins.maintenance-mode.config]
enabled = false
Testing Locally
Use the built-in plugin test runner:
bext plugin test
This loads your plugin in an isolated QuickJS context and runs the tests in test/plugin.test.js:
// test/plugin.test.js
describe("maintenance-mode", () => {
it("returns 503 when enabled", () => {
plugin.call("onServerStart", { enabled: true });
const result = plugin.call("onRequest", {
path: "/dashboard",
method: "GET",
});
expect(result.action).toBe("respond");
expect(result.status).toBe(503);
});
it("passes through when disabled", () => {
plugin.call("onServerStart", { enabled: false });
const result = plugin.call("onRequest", {
path: "/dashboard",
method: "GET",
});
expect(result.action).toBe("continue");
});
it("always allows health checks", () => {
plugin.call("onServerStart", { enabled: true });
const result = plugin.call("onRequest", {
path: "/healthz",
method: "GET",
});
expect(result.action).toBe("continue");
});
});
Debugging
Enable verbose plugin logging with the BEXT_PLUGIN_LOG environment variable:
BEXT_PLUGIN_LOG=debug bext dev
This prints all console.* output from your plugin, plus sandbox resource usage (memory, fetch count, storage bytes).
To inspect the KV store contents at runtime:
bext plugin storage list maintenance-mode
bext plugin storage get maintenance-mode enabled
Next Steps
- QuickJS Plugins — full API reference for the JS sandbox
- WASM Plugins — build high-performance plugins in Rust
- Publishing — share your plugin on plugins.bext.dev