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_timeoutnow actually preempts a spinning guest (WithCloseOnContextDone), andmemory_limitcaps 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 yourfail_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
- ABI v3 — rebuild required. Hosts reject bundles built with older SDKs (clear error at activation). Migration is mechanical:
SetHeader/AddHeader/DeleteHeader→SetRequestHeader/…orSetResponseHeader/…(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{...}).
- Bundle ID format changed to
sha256-<digest>. Existing channels and bundles keep loading; new builds get content-addressed IDs. checksum.txtis deprecated (still written and verified);descriptor.jsonis 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.