Skip to content
Esc
navigateopen⌘Jpreview
On this page

Switchboard v0.1.0

Switchboard bundles are now content-addressed, tested before they serve, and reversible — and the runtime that executes them finally has real teeth: enforced timeouts, memory caps, and validated guest output.

This is the largest release since the prototype. It breaks the guest ABI (rebuild your rules with the new SDK — one compile error per call site, details below) in exchange for a request/action model that composes with Caddy instead of working around it.

Highlights

Test rules before they ever reach the edge

switchboard build     # embeds tests.yaml into the bundle
switchboard test      # runs the bundle against its behavioral contract
switchboard eval --method GET --path /admin -H "x-user-role: viewer"

Test suites travel inside the bundle and are hashed into its identity. Every proxy re-runs the suite against the candidate runtime before activation — activation now means “this exact artifact passed its declared contract under the exact runtime that will serve traffic,” not “the module compiled.”

switchboard replay goes further: stream captured Caddy access logs through the current and candidate bundles offline and get a full decision diff (changed decisions, new denials, changed rewrites, candidate errors, p50/p99 execution time) — with --fail-on-new-denials for CI. Because rules have no body access, no network, and no mutable state, replay reproduces production decisions exactly.

Idempotent, reversible deploys

Bundle IDs are now sha256-<digest> over a canonical descriptor covering the module, manifest identity, and test suite. Identical builds produce identical IDs; re-deploying unchanged content skips the upload. Every deploy appends an immutable revision, and:

switchboard status   --channel prod
switchboard history  --channel prod
switchboard diff     prod ./dist --tests
switchboard promote  --from staging --to prod
switchboard rollback --channel prod

Concurrent deployers are serialized with conditional writes. Rollbacks roll forward — history is never rewritten. The descriptor’s signatures field is reserved for Ed25519/Sigstore signing in a future release.

A runtime that can say no

  • invoke_timeout now actually preempts a spinning guest (WithCloseOnContextDone), and memory_limit caps guest memory (default 32 MiB instead of the previous 4 GiB ceiling).
  • Every guest-produced value is validated: status-code ranges, RFC 7230 header tokens, CRLF/NUL rejection, output quotas (max_action_bytes, max_header_ops, max_response_body). Invalid actions are rejected, not clamped, and flow through your fail_mode.
  • A bundle that fails compile, validation, or its tests is quarantined until the channel pointer changes — one broken deployment no longer causes a fleet-wide compile storm every poll tick.
  • Replaced runtimes are retired after in-flight requests drain, fixing a pool leak on every third-and-later activation.
  • New fail_mode last_good: on a trap or timeout, retry the previous runtime before failing open/closed.

Survives registry outages

With cache_dir, every activated bundle is persisted locally (fsync + atomic rename). A restarted proxy verifies and serves its cached bundle before touching the registry — the machine that served a known-good bundle five seconds ago keeps serving it while your object store is down. The startup BucketExists preflight is gone, and wazero’s compilation cache makes restarts skip recompiling unchanged modules.

An ABI that composes with Caddy

Requests now expose Host, Query(name), Scheme, Protocol, RemoteAddr, ClientIP (honoring trusted_proxies), TLS, and Cookie(name). Actions gain Respond(status, body) for synthetic responses, explicit request-vs-response header operations, host/path/query rewrites, WithReason (visible in logs, tests, and replay diffs), and SetMetadata — which becomes Caddy request variables:

return sdk.Next().SetMetadata("backend", "v2").WithReason("v2-canary")
@v2 vars backend v2
reverse_proxy @v2 app-v2:8080
reverse_proxy app-v1:8080

Canary routing, tenant sharding, and cache policy without Switchboard implementing any proxying. Decisions and reasons also land in access logs (switchboard_decision, switchboard_reason, switchboard_bundle_id).

Try it in one binary

switchboard serve --listen :8080 --upstream localhost:3000 \
  --registry file://./registry --channel dev

New file:// and read-only https:// registries mean no MinIO required for development. serve exposes /switchboard/status and Prometheus /metrics, and the same middleware embeds in any Go HTTP server.

Breaking changes

  1. ABI v3 — rebuild required. Hosts reject bundles built with older SDKs (clear error at activation). Migration is mechanical:
    • SetHeader/AddHeader/DeleteHeaderSetRequestHeader/… or SetResponseHeader/… (choose which side you meant — the old API silently switched meaning per action type, which is exactly why it’s gone).
    • sdk.NewRequest(method, path, headers)sdk.NewRequest(sdk.RequestData{...}).
  2. Bundle ID format changed to sha256-<digest>. Existing channels and bundles keep loading; new builds get content-addressed IDs.
  3. checksum.txt is deprecated (still written and verified); descriptor.json is authoritative.

New examples

admin-gate, legacy-redirects, security-headers, maintenance-mode, and ab-canary-routing — each with a behavioral test suite that runs in CI and at activation.

Roadmap (v0.2.0)

Shadow candidate evaluation, parameterized modules + immutable data bundles, OCI registry support, bundle signing over the (already reserved) descriptor signatures field, and a Caddy admin API status module.

Full details in CHANGELOG.md.

Last updated on July 16, 2026

Was this page helpful?