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.typequickjs, 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