Building Your First bext Plugin in 10 Minutes
bext's plugin system lets you extend the server without forking the codebase. Plugins can intercept requests, transform responses, implement custom cache backends, or hook into the deployment lifecycle.
There are three plugin tiers, each with different tradeoffs:
| Tier | Languages | Startup | Isolation | Use for |
|---|---|---|---|---|
| QuickJS | JavaScript | <1ms | In-engine JS sandbox | Quick middleware, transforms, rules |
| WASM | Rust, Go, C, AssemblyScript | <1ms | Memory-safe Wasmtime sandbox | Compiled or perf-critical logic |
| nsjail | Python, Shell, any binary | ~10ms | OS process isolation (nsjail) | Heavier tools and existing scripts |
This tutorial uses the QuickJS runtime — the fastest way to get a JavaScript plugin running. The concepts apply to all three.
What Plugins Can Do
Plugins implement one or more interfaces:
- MiddlewarePlugin — runs before the request handler (auth, rate limiting, header injection)
- TransformPlugin — modifies the response body (HTML rewriting, asset injection)
- CacheBackend — provides a custom cache store (Redis, Memcached, S3)
- LifecyclePlugin — hooks into deploy, startup, and shutdown events
Middleware and transform plugins sit on either side of your route handler:
flowchart LR Req(["Request"]) --> MW["Middleware (on_request)"] MW -->|null: continue| H["Route handler"] MW -->|response: short-circuit| OUT(["Response"]) H --> TR["Transform (rewrite HTML)"] TR --> MR["Middleware (on_response)"] MR --> OUT classDef p fill:#fff1f2,stroke:#f43f5e,color:#0c0c0c,stroke-width:1.5px class MW,TR,MR p
Step 1: Create the Files
A plugin is just a directory with a manifest and an entry file. Create plugins/my-header-injector/:
plugins/my-header-injector/
manifest.toml
index.jsStep 2: Write the Plugin
Open index.js:
// Middleware plugin: add custom headers to every response
export function onRequest(req) {
// Return null to continue to the next handler
return null;
}
export function onResponse(req, res) {
// Add a custom header
res.headers["X-Powered-By"] = "bext";
res.headers["X-Request-Id"] = crypto.randomUUID();
return res;
}The plugin API is intentionally simple. onRequest receives the request and can return a response (short-circuit) or null (continue). onResponse receives both and can modify the response before it's sent.
Step 3: Configure
In manifest.toml:
name = "my-header-injector"
version = "0.1.0"
type = "MiddlewarePlugin"
tier = "quickjs"
priority = 100
[permissions]
network = false
filesystem = falseThe priority field controls execution order — lower numbers run first. Permissions are opt-in: a plugin that doesn't need network access doesn't get it.
Step 4: Run It
bext loads plugins from your project's plugins/ directory at startup, so there's nothing to install for a local plugin — just run the app:
bext run .Now every response includes X-Powered-By: bext and a unique request ID.
A More Useful Example: Bot Protection
// Block known bad bots based on User-Agent patterns
const BAD_BOTS = [
/AhrefsBot/i,
/SemrushBot/i,
/MJ12bot/i,
/DotBot/i,
];
export function onRequest(req) {
const ua = req.headers["user-agent"] || "";
for (const pattern of BAD_BOTS) {
if (pattern.test(ua)) {
return {
status: 403,
headers: { "Content-Type": "text/plain" },
body: "Forbidden",
};
}
}
return null; // Allow request to continue
}This runs in a QuickJS sandbox — even if the bot regex has a catastrophic backtracking bug, it can't crash the server. The sandbox enforces a CPU time limit (default: 50ms per request).
Transform Plugin: Inject Analytics
// Inject a script tag before </body>
const SCRIPT = '<script defer src="https://analytics.example.com/script.js"></script>';
export function transform(html) {
return html.replace("</body>", SCRIPT + "</body>");
}Transform plugins receive the rendered HTML and return modified HTML. They run after SSR but before compression, so the overhead is minimal.
Sharing Your Plugin
A plugin in your plugins/ directory is yours alone. To share one, compile it to the portable format — a single .wasm file — and distribute it through registry.bext.dev.
Before shipping, inspect the built artifact to review its manifest, declared permissions, and size:
bext plugins inspect my-header-injector.wasmAnyone can then drop it into their own server:
bext plugins install my-header-injector.wasmWhat's Next
For WASM plugins (Rust, Go, C), see the WASM plugin guide. For nsjail plugins (Python, Shell), see the nsjail guide. The plugin API reference is at docs.bext.dev/plugins/overview.