switchboard

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: MIT Imports: 0 Imported by: 0

README

Switchboard

Switchboard is an open-source prototype for programmable reverse proxy rules.

Users write request rules in Go, compile them to WebAssembly with TinyGo, upload immutable bundles to object storage, and proxy instances poll a channel pointer to hot-swap the active rule without restarting the proxy.

Switchboard keeps the long-lived dataplane stable, moves fast-changing request policy into versioned Wasm guests, and activates versions only after validation. It applies that pattern to ordinary reverse proxy deployments instead of requiring a custom CDN or edge network.

Rule deployments do not restart the proxy. Bundles are downloaded, verified, compiled, warmed, and validated off the request path, then activated with an atomic swap. In-flight requests continue using the runtime version they started with. If activation fails, the last known-good version remains active.

Shape

  • Caddy handler module: http.handlers.switchboard
  • CLI: switchboard init, switchboard build, switchboard dist, switchboard deploy, switchboard inspect
  • Registry: S3-compatible object storage only for deploy/inspect/load
  • Runtime: wazero
  • Guest rules: TinyGo WASI modules with a small host-function ABI
  • Reconciliation: background channel polling with last-known-good fallback

Install

Run the Caddy image with Switchboard already built in:

docker pull ghcr.io/ethndotsh/switchboard-caddy:latest

Install the CLI onto your PATH:

go install github.com/ethndotsh/switchboard/cmd/switchboard@latest

For local development from this repository:

go install ./cmd/switchboard

Install TinyGo too: https://tinygo.org/getting-started/install/.

Quickstart

Create a rule project:

mkdir my-rules
cd my-rules
switchboard init --name my-rules --registry s3://switchboard/prod

This writes:

go.mod
switchboard.yaml
rules/basic/rule.go

Rule projects use plain Go packages. switchboard build generates the TinyGo/Wasm export wrapper:

package basic

import "github.com/ethndotsh/switchboard/sdk"

func Handle(req sdk.Request) sdk.Action {
	if req.Path == "/blocked" {
		return sdk.Deny(403)
	}

	req.Headers["x-powered-by"] = []string{"switchboard"}
	return sdk.Next(req)
}

Build a distributable bundle:

switchboard build

or equivalently:

switchboard dist

build runs go mod tidy before invoking TinyGo so the generated SDK import is resolved. Use --skip-tidy in locked-down CI after dependencies are already pinned.

Deploy the bundle:

switchboard deploy

Inspect the active channel:

switchboard inspect

switchboard.yaml supplies the defaults:

name: my-rules
rule: ./rules/basic
dist: ./dist
namespace: customer-a
channel: prod
registry: s3://switchboard/prod

Environment variables are expanded before the YAML is parsed:

name: ${SWITCHBOARD_NAME:-my-rules}
rule: ${SWITCHBOARD_RULE:-./rules/basic}
dist: ${SWITCHBOARD_DIST:-./dist}
namespace: ${SWITCHBOARD_NAMESPACE:-customer-a}
channel: ${SWITCHBOARD_CHANNEL:-prod}
registry: s3://${SWITCHBOARD_S3_BUCKET}/${SWITCHBOARD_S3_PREFIX:-prod}

Supported forms are $VAR, ${VAR}, ${VAR:-fallback} for unset or empty values, and ${VAR-fallback} for unset values only.

Object Storage

Switchboard expects an S3-compatible registry. For local development, run MinIO or use any S3-compatible endpoint.

Required environment variables:

SWITCHBOARD_S3_ENDPOINT=localhost:9000
SWITCHBOARD_S3_ACCESS_KEY=minioadmin
SWITCHBOARD_S3_SECRET_KEY=minioadmin
SWITCHBOARD_S3_BUCKET=switchboard
SWITCHBOARD_S3_INSECURE=true

Objects use this layout:

channels/prod.json
bundles/2026-06-19T12-00-00Z-abc123/module.wasm
bundles/2026-06-19T12-00-00Z-abc123/manifest.json
bundles/2026-06-19T12-00-00Z-abc123/checksum.txt

Namespaces are optional. Without a namespace, Switchboard keeps the global layout above. With namespace: customer-a, channels and bundles are isolated under:

namespaces/customer-a/channels/prod.json
namespaces/customer-a/bundles/2026-06-19T12-00-00Z-abc123/module.wasm
namespaces/customer-a/bundles/2026-06-19T12-00-00Z-abc123/manifest.json
namespaces/customer-a/bundles/2026-06-19T12-00-00Z-abc123/checksum.txt

Registry URL prefixes remain a base path. registry: s3://switchboard/acme plus namespace: edge writes under acme/namespaces/edge/....

Repository Example

Build a rule:

go run ./cmd/switchboard build --out ./dist ./examples/basic

Deploy it to object storage:

go run ./cmd/switchboard deploy ./dist --channel prod

Inspect the active channel pointer:

go run ./cmd/switchboard inspect --channel prod

Rule Chains

A Switchboard bundle is a normal Go package. It needs one Handle(req sdk.Request) sdk.Action function, but the rule logic can live across as many files as you want:

rules/public/
  rule.go
  security.go
  routing.go
  headers.go

rule.go is the top-level ordering file:

func Handle(req sdk.Request) sdk.Action {
	return sdk.Chain(req,
		BlockInternalPaths,
		RewriteLegacyPaths,
		AddRuleHeader,
	)
}

The other files define ordinary Go functions:

func BlockInternalPaths(req sdk.Request) sdk.Action {
	if req.Path == "/internal" {
		return sdk.Deny(404)
	}
	return sdk.Next(req)
}

Build the whole package:

switchboard build ./rules/public

To use different rule packages for different Caddy routes, deploy them to different channels and attach the channel where that route lives:

example.com {
	route /admin/* {
		switchboard {
			registry s3
			namespace customer-a
			channel admin
		}

		reverse_proxy admin:3000
	}

	route {
		switchboard {
			registry s3
			namespace customer-a
			channel public
		}

		reverse_proxy app:3000
	}
}

Each route gets one atomic active bundle. Namespace groups channels; channel remains the stable deployment pointer. Inside a bundle, ordering is explicit Go code.

Caddy

Use the published Caddy image:

docker run --rm -p 8080:8080 \
	-v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" \
	-e SWITCHBOARD_S3_ENDPOINT \
	-e SWITCHBOARD_S3_ACCESS_KEY \
	-e SWITCHBOARD_S3_SECRET_KEY \
	-e SWITCHBOARD_S3_BUCKET \
	ghcr.io/ethndotsh/switchboard-caddy:latest

Build with xcaddy:

xcaddy build --with github.com/ethndotsh/switchboard/caddy@latest

For local development from this repository:

xcaddy build --with github.com/ethndotsh/switchboard/caddy=./caddy

Caddyfile:

:8080 {
	route {
		switchboard {
			registry s3
			namespace customer-a
			channel prod
			poll_interval 2s
			pool_size 16
			fail_mode open
		}

		reverse_proxy localhost:9000
	}
}

The handler never downloads, compiles, or instantiates bundles on the request path. A background reconciler polls channels/{channel}.json, downloads immutable bundles, verifies checksums, compiles Wasm, warms the configured guest pool, validates the candidate, and atomically swaps the active runtime.

If the warmed pool is exhausted on the request path, Switchboard treats the rule as unavailable and applies fail_mode.

Docker

Published images are available at:

ghcr.io/ethndotsh/switchboard-caddy:latest
ghcr.io/ethndotsh/switchboard-caddy:v0.1.0
ghcr.io/ethndotsh/switchboard-caddy:sha-<commit>

Build a Caddy image with the Switchboard module from a checkout:

docker build -t switchboard-caddy .

Pin the public Go module version by overriding SWITCHBOARD_VERSION:

docker build \
	--build-arg SWITCHBOARD_REPLACE= \
	--build-arg SWITCHBOARD_VERSION=latest \
	-t switchboard-caddy .

The default Docker build uses the checkout copied into the image build context. The pinned public-module mode clears SWITCHBOARD_REPLACE, then resolves github.com/ethndotsh/switchboard through Go modules.

GitHub Actions publishes the Caddy image from the checked-out source commit on pushes to the default branch and version tags. Pull requests build the image without publishing it.

Performance Notes

Switchboard optimizes for predictable request-path behavior rather than zero-cost rule execution. Bundles are reconciled, compiled, warmed, and validated off-path; request handling borrows an already-warmed Wasm instance from the active runtime pool.

Local benchmarks on a warmed pool show no-op and simple block rules around 14 us/op in-process. In the Docker e2e workspace, sequential local HTTP pass-through was in the same millisecond range as stock Caddy reverse proxying to the same Python backend. These numbers are directional, not production guarantees; real latency depends on host, pool sizing, traffic shape, rule complexity, and backend behavior.

Prior Art

Switchboard borrows architectural lessons from Railway's Hikari CDN writeup: keep the host dataplane stable, move request policy into versioned guests, reconcile toward desired state, validate candidates off-path, and activate with an atomic swap. See Railway's Hikari CDN architecture.

TinyGo

Install TinyGo from https://tinygo.org/getting-started/install/.

The build command shells out to:

tinygo build -target=wasi -o dist/module.wasm ./examples/basic

Limitations

  • Request body and response body mutation are intentionally out of scope.
  • There is no hosted control plane.
  • Caddy is the reference adapter.
  • The ABI is intentionally small and will likely change.
  • Registry operations require S3-compatible object storage credentials.
  • Switchboard is not a CDN, cache, BGP system, anycast network, or hosted control plane.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action struct {
	Type        ActionType        `json:"type"`
	StatusCode  int               `json:"status_code,omitempty"`
	Location    string            `json:"location,omitempty"`
	RewritePath string            `json:"rewrite_path,omitempty"`
	Headers     map[string]string `json:"headers,omitempty"`
}

type ActionType

type ActionType string
const (
	ActionNext     ActionType = "next"
	ActionDeny     ActionType = "deny"
	ActionRedirect ActionType = "redirect"
	ActionRewrite  ActionType = "rewrite"
)

type Request

type Request struct {
	Method  string              `json:"method"`
	Path    string              `json:"path"`
	Headers map[string][]string `json:"headers"`
}

Directories

Path Synopsis
abi
adapters
cmd
switchboard command
e2e
rules/v1 command
rules/v2 command
examples
basic command
chained command
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL