Switchboard v0.2.0
Rules can finally look things up — allowlists, redirect maps, cohort tables, typed config — without giving up a shred of the determinism that makes them safe. And the three ways to run Switchboard (standalone, Caddy, embedded) now behave the same, down to the metrics.
A smaller release than v0.1.0, but it breaks the guest ABI again (v3 → v4). This time the migration is nothing but a recompile: the ABI only gained functions, so there are no source changes to make — rebuild your rules with the new SDK and you’re done.
Highlights
Rules can look things up
Ship read-only files alongside a rule and read them at request time:
my-rules/
rule.go
data/allowlist.txt # picked up automatically, or --data ./dir
func Handle(req sdk.Request) sdk.Action {
if sdk.DataSet("allowlist.txt").Contains(req.ClientIP()) {
return sdk.Next()
}
return sdk.Deny(403).WithReason("not-allowlisted")
}
Data files are embedded at build, hashed into the bundle’s identity, and digest-verified on every read — so changing a lookup table ships as a new immutable bundle that passes its embedded tests before activating and rolls back like any code change. A decision is still a pure function of the request, so replay stays exact. Total size is bounded by max_data_bytes (default 4mb), enforced at build and as an activation gate.
One accessor per format, and you only pay for what you use
type Flags struct {
Maintenance bool `json:"maintenance"`
BlockedPaths []string `json:"blocked_paths"`
}
var f Flags
if err := sdk.DataJSON("flags.json", &f); err != nil { /* ... */ }
DataJSON, DataJSONL, DataTOML, DataCSV, DataSet, DataLines, DataMap, DataBytes, DataString. Each parser is linked into the guest only when its accessor is called — a DataSet-only rule stays ~50 KB while a DataTOML rule links the TOML parser and grows. A missing file is never an error. (LoadTestData injects data for host-side rule tests. YAML is intentionally absent — yaml.v3 fails at runtime under TinyGo; use JSON or TOML.)
serve reaches parity with Caddy
The standalone binary was missing two-thirds of the engine’s knobs. No longer:
switchboard serve --upstream localhost:3000 --registry file://./registry \
--fail-mode last_good --fallback-fail-mode closed \
--memory-limit 64mb --max-response-body 16kb \
--pool-autoscale on --min-pool-size 8 --max-pool-size 64
Every Caddyfile directive now has a matching serve flag (kebab-case), resolved through the same validation. serve also stops silently dropping rule metadata — decisions are surfaced as structured log fields, matching Caddy.
Metrics and status, whatever the adapter
The Caddy handler used to record zero switchboard_* metrics — its request path bypassed the instrumented middleware entirely. Now the same collectors back both adapters:
switchboard {
registry s3
channel prod
status_path /switchboard/status
}
Caddy registers switchboard_invocations_total, switchboard_pool_instances, switchboard_activation_total and the rest on its own /metrics, and serves the status JSON at status_path. Standalone and embedded expose the identical metric set and a shared StatusHandler.
Breaking changes
- ABI v4 — rebuild required, no code changes. Hosts reject bundles built with an older SDK (clear error at activation). Unlike v3, nothing in your rules changes:
switchboard buildwith the current SDK is the whole migration.
New examples
ip-allowlist (DataSet membership from a bundled list) and feature-flags (typed struct config via DataJSON) — each with a behavioral test suite that runs at activation. New guide: Bundle data files.