janus

package module
v1.7.2 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 63 Imported by: 0

README

Janus Logo

Caddy module: long-lived edge server — TLS admission, dynamic host routing, registry-driven upstreams, heartbeats, on-demand TLS asks, a generation-fenced micro-cache with request coalescing, edge-terminated WebSocket fan-out, zero-config LAN presence over mDNS, an edge authentication wall, registered static files, X-Sendfile offload, and bounded app-scoped access observation, driven by a JSON control API.


Module names: janus (app) · http.handlers.janus (HTTP handler) · caddy.logging.encoders.janus (access encoder)

Janus is a Caddy module. Caddy provides listeners, HTTP/1–3, TLS, and ACME. Janus provides the inward face: a memory-resident registry and engines driven by the /1.0 JSON API. Cold Caddyfile config sets capabilities (such as control reachability) and which sites admit traffic into Janus; hot /1.0 calls decide how admitted hosts map to upstreams, health, certificate allowlisting, and realtime fan-out.

{
	janus {
		ping
		control local
		cache
		hub
		mdns
		files {
			precompressed
		}
	}
}

app.example.com {
	log {
		output file /var/log/janus/access.json
		format janus
	}
	janus
}

Registry, data plane, and hub state live in pooled process state: a Caddy config reload never drops a registration or a WebSocket. Everything is memory-only by contract — a restart empties the registry and tenants re-register. See Caddyfile.minimal for the operator-facing starting point, Caddyfile.example for the full capability walkthrough, and docs/ for the contracts.

This repository is a Go module. Caddy is a dependency, not a git submodule. A Janus-enabled binary is produced with xcaddy, which compiles stock Caddy together with this module into one static caddy binary.

License: Apache License 2.0 (same family as Caddy’s source).

What Janus is — and is not

Every capability in Janus has a famous neighbor; the mix has none. The novel contract is admission itself: the app announces itself to its own edge. A tenant POSTs its name and hosts to /1.0/apps, heartbeats, and publishes worker unix sockets — and with that one registration it has TLS and ACME, HTTP/1–3, host routing, health-aware least-conn balancing, an app-steerable micro-cache, edge-terminated WebSocket fan-out, and LAN presence, with zero per-app edge configuration. That is the router contract of a PaaS — the shape of Fly's proxy or Heroku's router — in one self-hosted binary, with the running app as the source of truth and heartbeat reaping as the garbage collector: an app that stops heartbeating simply ceases to exist at the edge. The nearest historical relative is Phusion Passenger, the app-aware web server — but Passenger manages processes for its supported languages and learns about apps from the web server's own config; Janus speaks a JSON API and learns about apps from the apps.

Each neighbor is better at being itself. The honest comparison:

Neighbor What it does better What Janus does instead
Traefik Provider ecosystem — routing derived from Docker labels, Kubernetes Ingress/CRD/Gateway API, Consul, Nomad, ECS — plus a deep middleware catalog and a community that dwarfs this module Apps register themselves over plain HTTP; no container runtime, orchestrator, or label convention required — a bare process on a unix socket is a first-class tenant
Varnish Cache policy as a product: VCL compiled to native code, grace mode, ESI, bans, now native TLS A deliberately small micro-cache — 1s default TTL, request coalescing, generation-fenced purge on every upstream swap, hard bypass on Cookie/Authorization — honest speed for dynamic anonymous GETs, not a policy engine
Pushpin Protocol range for realtime: HTTP streaming, long-polling, SSE, SockJS, WebSocket-over-HTTP against a stateless backend The same architectural instinct — connections held at the proxy, tenant on plain HTTP — plus registry integration (an app's hub lives and dies with its registration) and a validated per-frame directive grammar executed at the edge
Caddy Everything it already is: listeners, ACME, HTTP/1–3, the Caddyfile, the admin API, the module ecosystem — all of it remains available beside Janus in the same process A second axis of dynamism: Caddy's admin API pushes operator config; the Janus registry pulls state from running apps, and a registration never touches the config

Traefik answers "what is my orchestrator running?"; Janus answers "what is announcing itself to me right now?" — the second question needs no infrastructure underneath the app. Varnish is the right tool when cache policy is the point; the Janus cache exists to make a stampede on a dynamic page cost the worker ~1 request per second, and it measures ~410x on capacity-bound routes and 1.6–1.8x on trivial ones (the performance ledger holds every number with raw provenance — sustained hub fan-out is ~0.4M deliveries/s, roughly independent of room size, with zero socket drops across a config reload). Pushpin proved the edge-held-connection pattern at Fastly scale; the hub is that pattern folded into the registry. And Caddy is not a competitor at all: Janus is a Caddy module, and every stock directive works unchanged next to it.

Janus is not:

  • a reverse-proxy configuration language. There is no parallel grammar — capabilities are normal Caddyfile directives with legal values, defaults, and hard errors, same as stock Caddy.
  • a persistent store. Memory-only by contract: a restart empties the registry and tenants re-register. Nothing is written to disk.
  • a container orchestrator. Janus never starts, stops, or supervises a process. Tenants run themselves; Janus routes to what is alive.
  • a CDN or cache appliance. The micro-cache shields workers from request volume; it does not do cache hierarchies, edge networks, or operator-authored policy.
  • a service mesh. One edge, inward-facing unix sockets — no sidecars, no inter-service mTLS fabric, no traffic policy between tenants.

The same binary spans the whole distance: janus.local answering a phone on a bare LAN with no DNS and no client install, and a production edge with ACME certificates and HTTP/3 — the difference is only Caddyfile. And with rip-server as the tenant, the same app file that runs standalone on a laptop registers, heartbeats, and pools behind Janus in production, unchanged.

Requirements

  • Go — current stable release (go.dev/dl)
  • xcaddy — builds Caddy with modules
  • A Caddyfile that loads Janus (repo root)

Install Go (macOS, Homebrew)

brew update
brew install go          # or: brew upgrade go
go version               # confirm current stable

Install Go (official tarball)

Follow go.dev/doc/install. On macOS Apple Silicon, that is typically the darwin-arm64 archive or .pkg from go.dev/dl. Ensure $(go env GOPATH)/bin is on your PATH so tools installed with go install are available.

Install xcaddy

go install github.com/caddyserver/xcaddy/cmd/xcaddy@v0.4.7
xcaddy version

Capability order

Cold capabilities land in order. Each step stands alone before the next is added.

# Capability What it does Doc
1 ping Proves module load, TLS, site admission, cascade capability-ping
2 control Opens the /1.0 listeners (internal/local/public) capability-control
3 cache Site-scoped micro-cache + request coalescing on the data plane capability-microcache
4 hub Per-app WebSocket fan-out terminated at the edge; tenants observe and steer over HTTP capability-hub
5 mdns LAN presence: janus.local + per-app .local names over multicast DNS, and the read-only status front door capability-mdns
6 auth URL-prefix gates for auth-less apps: shared users, per-gate allow lists, host-wide session, Remote-User strip-and-inject capability-auth
7 files Registered ordered roots, transparent precompressed sidecars, SPA shells, and directory-gated site hosts capability-files, precompressed extension
8 sendfile Always-on final upstream X-Sendfile transformation with validators, ranges, cache recording, and streaming capability-sendfile
9 browse Navigable hot and cold roots, content-addressed themes, bounded extension renderers, and process leases capability-browse
10 access log JSON-compatible durable access log plus bounded app-scoped NDJSON streams on /1.0 capability-access-log
export PATH="$(go env GOPATH)/bin:$PATH"

go mod tidy
mkdir -p bin
xcaddy build \
  --with github.com/shreeve/janus=. \
  --replace github.com/go-chi/chi/v5=github.com/go-chi/chi/v5@v5.3.2 \
  --output ./bin/caddy-janus

go test ./...
./test.sh   # capability-ordered acceptance groups, ending with access

The chi replacement is an intentional security pin for the prebuilt Caddy binary; Caddy 2.11.4 otherwise resolves chi 5.2.5.

1. ping (data plane)

Trusted wildcard cert in certs/; DNS → 127.0.0.1; SNI picks the site. No control plane required.

./bin/caddy-janus run
curl -s https://foo.ripdev.io/ping          # catchall → pong
curl -s https://on.ripdev.io/ping           # explicit on → pong
curl -s -o /dev/null -w '%{http_code}\n' https://off.ripdev.io/ping
# → 404

On some systems binding :443 needs elevated privileges (sudo ./bin/caddy-janus run …). On current macOS it often works without sudo.

2. control (/1.0)

Same process. Loopback HTTP and a unix socket serve the control API.

curl -s http://127.0.0.1:7600/1.0
curl -s http://127.0.0.1:7600/1.0/health
curl -s --unix-socket run/janus.sock http://janus/1.0

3. cache

Anonymous GETs on registered hosts answer from memory for one TTL; concurrent misses coalesce into one worker request; every upstream swap purges.

curl -s http://127.0.0.1:7600/1.0/cache     # hit/miss/coalesce counters

4. hub

WebSocket upgrades on hub-enabled sites terminate at Janus; JSON directive frames fan out per app at the edge, so app reloads never drop a socket. The tenant registers a bridge to observe frames and steer, and publishes through the control plane.

curl -s http://127.0.0.1:7600/1.0/hub       # fan-out / bridge counters
curl -s -X POST -H 'Content-Type: application/json' \
  --data '{"@":["/lobby"],"news":{"v":1}}' \
  http://127.0.0.1:7600/1.0/apps/$APP_ID/hub/publish

5. mdns

Opt-in LAN presence: janus.local (and every registered single-label .local host) answers over multicast DNS with no DNS server or client install, and a plain-HTTP front door serves a read-only, self-contained status page — registry, worker health, heartbeat freshness, cache and hub counters, socket paths redacted. An optional canonical origin turns the page into a hand-off ramp to real HTTPS, with a built-in diagnostic for router DNS-rebinding filters.

curl -s http://127.0.0.1:7600/1.0/mdns      # advertiser state (names, states, counters)
curl -s -H 'Host: janus.local' http://127.0.0.1:7680/status.json

6. auth

URL-prefix gates in front of tenant apps that have no login story of their own: define a shared users table and one or more gate <path> { … } allow lists (credentials minted by caddy janus-auth-hash). Each gate's login door is exact {prefix}auth. One host-wide session — sign in once, sign out once; a request under a gate proceeds only if the session user is on that gate's allow list. Longest prefix wins; paths outside every gate stay open. What passes a gate carries Remote-User: <name>; cookies and client Remote-User are stripped on every fall-through. Sessions live in memory: unchanged reloads keep eligible sessions, removing a user or host revokes its sessions after commit, and a restart signs everyone out. Admins observe and revoke over /1.0/auth.

./bin/caddy-janus janus-auth-hash                 # mint a version-a passhash (password prompted, never argv)
curl -s http://127.0.0.1:7600/1.0/auth      # wall counters + session count
curl -s http://127.0.0.1:7600/1.0/auth/sessions

10. access log

Each Janus site opts into Caddy access logging with format janus. Durable output is byte-equivalent to Caddy's JSON encoder with the same options. Registered-app requests also publish bounded NDJSON to operator streams; Caddy policy remains authoritative, so entries excluded before encoder invocation publish nothing.

curl -s http://127.0.0.1:7600/1.0/access
curl -sN "http://127.0.0.1:7600/1.0/apps/$APP_ID/access?after=0"

Build and run

From this repository (local module replacement is automatic when you run xcaddy inside the module):

# Develop: build a temporary caddy+janus and run it
xcaddy run

# Produce a binary (see ping-only proof above)
xcaddy build \
  --with github.com/shreeve/janus=. \
  --replace github.com/go-chi/chi/v5=github.com/go-chi/chi/v5@v5.3.2 \
  --output ./bin/caddy-janus
./bin/caddy-janus run

From anywhere, against a published module version:

xcaddy build \
  --with github.com/shreeve/janus@main \
  --replace github.com/go-chi/chi/v5=github.com/go-chi/chi/v5@v5.3.2 \
  --output ./caddy

Pin Caddy and Janus versions for reproducible builds (replace versions as appropriate):

xcaddy build v2.11.4 \
  --with github.com/shreeve/janus@v1.7.2 \
  --replace github.com/go-chi/chi/v5=github.com/go-chi/chi/v5@v5.3.2 \
  --output ./caddy

Confirm the module is linked:

./bin/caddy-janus list-modules | grep janus

Prebuilt releases

The tagged release workflow publishes five self-contained archives:

Platform Archive
macOS Apple Silicon janus-<tag>-osx-arm64.tar.gz
Linux x86-64 janus-<tag>-linux-amd64.tar.gz
Linux ARM64 janus-<tag>-linux-arm64.tar.gz
Windows x86-64 janus-<tag>-windows-amd64.zip
Windows ARM64 janus-<tag>-windows-arm64.zip

Download the matching archive from the releases page and extract it. On macOS and Linux, run ./install.sh to install caddy-janus into /usr/local/bin, or choose another destination with BIN="$HOME/bin" ./install.sh. The extracted binary also runs in place. On Windows, run caddy-janus.exe directly. Each archive also contains Caddyfile.minimal, Caddyfile.example, the README, and the license; the installer deliberately leaves configuration in the archive rather than overwriting a live Caddyfile. The release's janus-<tag>-checksums.txt verifies every archive.

Release builds run on native GitHub runners and compile from the pushed tag, so list-modules --versions on a downloaded binary reports the exact Janus version. Pushing a v* tag runs the release workflow automatically.

For a local development build:

make janus              # working tree -> bin/caddy-janus
make unit               # fast Go test suite
make test               # build + unit + acceptance suite
make install            # build + install -> /usr/local/bin/caddy-janus
# make install BIN="$HOME/bin"

JSON config

The Caddyfile adapts to this JSON shape (all capability keys optional; unset keys cascade global → site → built-in default):

{
  "apps": {
    "janus": {
      "control": [{ "mode": "local" }],
      "ping": true,
      "cache": { "enabled": true, "ttl": "1s" },
      "hub": { "enabled": true, "path": "/hub", "max_conns": 4096 },
      "mdns": { "name": "janus.local" },
      "auth": { "enabled": true, "replace": true, "users": [{ "name": "alice", "credential": "a…" }], "gates": [{ "prefix": "/", "allow": ["alice"] }], "ttl": "8h" },
      "heartbeat_ttl": "15s"
    },
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "match": [{ "host": ["app.example.com"] }],
            "handle": [{ "handler": "janus" }]
          }]
        }
      }
    }
  }
}

Layout

Path Role
app.go Process-wide janus app (control, global defaults, pooled state)
handler.go Site http.handlers.janus (admission + site overrides)
caddyfile.go Caddyfile wiring: global janus block + site directive parsing, directive order
doc.go Package overview (the go doc face of the module)
state.go Pooled process state (registry, data plane, hubs survive reloads)
cascade.go Cascade helpers shared by every site-scoped capability
control.go Control listener config (control internal/local/public, token:…)
control_api.go Control listeners + /1.0 mux (meta, health, tls/ask)
control_hub.go Hub control surface (publish, snapshot, counters)
apps.go Hot apps registry (CRUD, upstreams, bridge, heartbeats, TTL sweep)
dataplane.go Host → worker-socket proxying (least-conn, health, marked 503s)
ring.go Doorbell ring: single-flight wake-up for dirty apps
cache.go Micro-cache store: shards, doorkeeper, LRU, purge, counters
cache_serve.go Cache request path: decision table, coalescing, the fill
cache_config.go cache directive: parse, cascade, provision
hub.go Hub state and executor (membership, delivery, counters)
hub_frame.go Hub wire grammar (sigils, events, whole-frame validation)
hub_conn.go Hub connection lifecycle (writer, backpressure, close paths)
hub_ws.go Hub WebSocket edge (admission, upgrade, reader)
hub_bridge.go Hub tenant bridge (per-connection FIFO, open/text/close POSTs)
hub_config.go hub directive: parse, cascade, site table, floors
mdns.go mDNS advertiser (pooled, reconcile goroutine) + status front door
mdns_config.go mdns directive: parse, provision, validation
mdns.html Embedded status page (self-contained; zero external resources)
control_mdns.go mDNS control surface (GET /1.0/mdns)
auth.go Auth wall: gates, pooled sessions, throttle ladder, CSRF, login doors
auth_config.go auth directive: users, gates, parse, cascade, passhash codec, site table
auth_cmd.go caddy janus-auth-hash credential minter
auth.html Embedded login/status page (self-contained; zero external resources)
control_auth.go Auth control surface (GET /1.0/auth, session list + revocation)
access.go Pooled access bridge, registration sequence state, bounded event schema
access_encoder.go caddy.logging.encoders.janus, wrapping durable JSON
access_stream.go Access status and app-scoped NDJSON control streams
access_writer.go Response outcome observation with optional interfaces preserved
testkit/ Go test-support program: fixtures + WS driver for test.sh
bench/ Committed bench harness (baseline, leak probe, hub arm)
Caddyfile Working cold config (multi-site cascade demos)
Caddyfile.minimal Operator-facing starting point: one app site, one browsable root (validates standalone)
Caddyfile.example Production-shaped walkthrough of every capability and knob (validates standalone)
test.sh High-level acceptance suite (self-contained; not a substitute for go test)
docs/ Contracts, capability pages, measurements (YYYYMMDD-HHMMSS- prefixed; see docs/README.md)

Design notes

See docs/ for the control-plane sketch and related material. The /1.0 API follows an Incus-inspired style (envelopes, resource paths) while remaining Janus’s own protocol; writes carry no fencing fields — the tenant serializes its own writes (see the pool protocol).

Name

In Roman myth, Janus is the god of doorways and thresholds — beginnings, passages, and the space between inside and outside. He is shown with two faces: one looking out, one looking in. That is the shape of this module. One face serves the public world over TLS; the other coordinates private upstreams, registry, and control-plane state so that serving is possible. The passage between them is the product.

Documentation

Overview

Package janus is a Caddy module that fronts disposable worker pools: cold Caddyfile capabilities on the data plane, a hot /1.0 control API on the control plane, and nothing durable in between.

Janus registers two Caddy modules: the app "janus" (process-wide control listeners and capability defaults, configured in the global options block) and the HTTP handler "http.handlers.janus" (per-site admission and capability overrides). Cold config admits capabilities; the hot registry wires tenants: apps register their hosts, publish their worker unix sockets, and heartbeat on /1.0, while Janus routes admitted requests host→upstream with doorbell-driven reloads that are invisible to clients.

Capabilities land in order: ping (1) proves the chassis, control (2) serves /1.0, cache (3) is a site-scoped micro-cache with request coalescing, hub (4) terminates WebSockets at the edge and fans JSON directive frames out per app while the tenant observes and steers over plain HTTP, mdns (5) advertises janus.local plus registered .local app hosts over multicast DNS and serves the read-only status front door, auth (6) is URL-prefix gates for auth-less apps: shared users, per-gate allow lists, one host-wide session, and Remote-User strip-and-inject on fall-through, and files (7) serves registered ordered roots and SPA shells with directory-gated site host patterns and trusted Rip-Site context. Sendfile (8) is an always-on reverse-proxy response protocol: a final application X-Sendfile instruction selects any regular file Janus can open, and Janus applies validators, ranges, cache recording, and streaming at the edge. Browse (9) turns selected hot and cold roots into navigable spaces with a content-addressed theme and bounded extension renderers. Access log (10) wraps Caddy's JSON encoder without changing durable bytes and publishes bounded app-scoped NDJSON through the control plane.

The registry, data plane, and hub state live in pooled process state (caddy.UsagePool), so a Caddy config reload never drops a registration or a hub socket; only registry DELETE, heartbeat TTL reap, or process exit tears them down. Everything is memory-only by contract — a restart empties the registry and tenants re-register.

The authoritative contracts live under docs/: the phased build spec, one page per capability, the Janus↔tenant pool protocol, and the performance ledger with raw bench provenance.

Index

Constants

View Source
const (
	DefaultControlInternal = "run/janus.sock"
	DefaultControlLocal    = "http://127.0.0.1:7600/"
	DefaultControlPublic   = "https://0.0.0.0:7601/"
)

Default listen targets when a control line omits the address.

Variables

This section is empty.

Functions

This section is empty.

Types

type AccessEncoder added in v1.5.0

type AccessEncoder struct {
	caddylogging.LogEncoderConfig
	// contains filtered or unexported fields
}

AccessEncoder preserves Caddy's JSON access log while publishing the bounded operator stream for entries carrying Janus request facts.

func (*AccessEncoder) AddArray added in v1.5.0

func (e *AccessEncoder) AddArray(key string, value zapcore.ArrayMarshaler) error

func (*AccessEncoder) AddBinary added in v1.5.0

func (e *AccessEncoder) AddBinary(key string, value []byte)

func (*AccessEncoder) AddBool added in v1.5.0

func (e *AccessEncoder) AddBool(key string, value bool)

func (*AccessEncoder) AddByteString added in v1.5.0

func (e *AccessEncoder) AddByteString(key string, value []byte)

func (*AccessEncoder) AddComplex64 added in v1.5.0

func (e *AccessEncoder) AddComplex64(key string, value complex64)

func (*AccessEncoder) AddComplex128 added in v1.5.0

func (e *AccessEncoder) AddComplex128(key string, value complex128)

func (*AccessEncoder) AddDuration added in v1.5.0

func (e *AccessEncoder) AddDuration(key string, value time.Duration)

func (*AccessEncoder) AddFloat32 added in v1.5.0

func (e *AccessEncoder) AddFloat32(key string, value float32)

func (*AccessEncoder) AddFloat64 added in v1.5.0

func (e *AccessEncoder) AddFloat64(key string, value float64)

func (*AccessEncoder) AddInt added in v1.5.0

func (e *AccessEncoder) AddInt(key string, value int)

func (*AccessEncoder) AddInt8 added in v1.5.0

func (e *AccessEncoder) AddInt8(key string, value int8)

func (*AccessEncoder) AddInt16 added in v1.5.0

func (e *AccessEncoder) AddInt16(key string, value int16)

func (*AccessEncoder) AddInt32 added in v1.5.0

func (e *AccessEncoder) AddInt32(key string, value int32)

func (*AccessEncoder) AddInt64 added in v1.5.0

func (e *AccessEncoder) AddInt64(key string, value int64)

func (*AccessEncoder) AddObject added in v1.5.0

func (e *AccessEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error

func (*AccessEncoder) AddReflected added in v1.5.0

func (e *AccessEncoder) AddReflected(key string, value any) error

func (*AccessEncoder) AddString added in v1.5.0

func (e *AccessEncoder) AddString(key, value string)

func (*AccessEncoder) AddTime added in v1.5.0

func (e *AccessEncoder) AddTime(key string, value time.Time)

func (*AccessEncoder) AddUint added in v1.5.0

func (e *AccessEncoder) AddUint(key string, value uint)

func (*AccessEncoder) AddUint8 added in v1.5.0

func (e *AccessEncoder) AddUint8(key string, value uint8)

func (*AccessEncoder) AddUint16 added in v1.5.0

func (e *AccessEncoder) AddUint16(key string, value uint16)

func (*AccessEncoder) AddUint32 added in v1.5.0

func (e *AccessEncoder) AddUint32(key string, value uint32)

func (*AccessEncoder) AddUint64 added in v1.5.0

func (e *AccessEncoder) AddUint64(key string, value uint64)

func (*AccessEncoder) AddUintptr added in v1.5.0

func (e *AccessEncoder) AddUintptr(key string, value uintptr)

func (AccessEncoder) CaddyModule added in v1.5.0

func (AccessEncoder) CaddyModule() caddy.ModuleInfo

func (*AccessEncoder) Cleanup added in v1.5.0

func (e *AccessEncoder) Cleanup() error

func (*AccessEncoder) Clone added in v1.5.0

func (e *AccessEncoder) Clone() zapcore.Encoder

func (*AccessEncoder) EncodeEntry added in v1.5.0

func (e *AccessEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error)

func (*AccessEncoder) OpenNamespace added in v1.5.0

func (e *AccessEncoder) OpenNamespace(key string)

func (*AccessEncoder) Provision added in v1.5.0

func (e *AccessEncoder) Provision(ctx caddy.Context) error

func (*AccessEncoder) UnmarshalCaddyfile added in v1.5.0

func (e *AccessEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

type App

type App struct {
	// Control is the exact set of control-plane listeners serving the
	// hot /1.0 API. Empty means one implicit internal (unix socket)
	// listener.
	Control []Control `json:"control,omitempty"`

	// Ping is the global default for the site-scoped ping capability.
	// Default: off. Sites may override.
	Ping *bool `json:"ping,omitempty"`

	// Cache is the global default and process-wide pool configuration
	// for the site-scoped micro-cache. Default: off. Sites may override
	// the per-site keys.
	Cache *CacheSettings `json:"cache,omitempty"`

	// Hub is the global default for the site-scoped hub capability
	// (per-app WebSocket fan-out). Default: off. Sites may override.
	Hub *HubSettings `json:"hub,omitempty"`

	// Mdns is the process-wide LAN-presence capability: the advertised
	// .local identity, per-app .local advertising, and the plain-HTTP
	// front door. Default: off (nil).
	Mdns *MdnsSettings `json:"mdns,omitempty"`

	// Auth is the global default for the site-scoped auth wall (edge
	// authentication for auth-less apps): default posture plus the
	// default user set. Default: off. Sites may override.
	Auth *AuthSettings `json:"auth,omitempty"`

	// Files is the global default for the site-scoped registered-file
	// service. Default: off. Sites may override.
	Files *bool `json:"files,omitempty"`

	// FilesPrecompressed is the process-wide ordered set of sidecar
	// representations admitted by the global files capability. Site
	// blocks may turn files on or off but cannot retune this order.
	FilesPrecompressed []string `json:"files_precompressed,omitempty"`

	// Browse is the process-wide theme and renderer configuration.
	// Presence also enables the global site-scoped browse default.
	Browse *BrowseSettings `json:"browse,omitempty"`

	// HeartbeatTTL is how long a registered app may go without a
	// heartbeat before its registration is reaped (same effect as
	// DELETE). Default: 15s. The JANUS_HEARTBEAT_TTL environment
	// variable is honored as a fallback when this is unset.
	HeartbeatTTL caddy.Duration `json:"heartbeat_ttl,omitempty"`
	// contains filtered or unexported fields
}

App is the process-wide Janus application (cold config).

func (*App) CaddyModule

func (*App) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*App) Cleanup

func (a *App) Cleanup() error

Cleanup first tears down this generation's resources, including when Caddy rejects a started candidate without calling Stop. It then releases the app's reference on the pooled state; the last release (process shutdown) destructs it. A release that leaves other generations holding the state is either a successful reload's old generation retiring or an aborted reload's new generation being torn down — the advertiser tells them apart and rolls an aborted generation back to its surviving predecessor.

func (*App) Provision

func (a *App) Provision(ctx caddy.Context) error

Provision sets up the app. Registry, data plane, and hub state come from the pooled process holder, so a config reload binds the new app to the same live state instead of constructing a split-brain registry.

func (*App) Start

func (a *App) Start() error

Start starts the Janus app.

func (*App) Stop

func (a *App) Stop() error

Stop stops the Janus app. Pooled state (registry sweeper, hubs, open sockets, the mdns advertiser) deliberately survives: a config reload stops the old app while the new one is already serving the same pooled state.

func (*App) UnmarshalCaddyfile

func (a *App) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile parses the global janus block.

type AppRecord

type AppRecord struct {
	ID        string       `json:"id"`
	Name      string       `json:"name"`
	Hosts     []string     `json:"hosts"`
	Upstreams []Upstream   `json:"upstreams"`
	Site      *SitePolicy  `json:"site,omitempty"`
	Files     *FilesPolicy `json:"files,omitempty"`
	Lease     string       `json:"lease"`

	// Bridge is the tenant's hub bridge endpoint (optional; empty =
	// hub handshakes answer 503). Cold config never carries it: which URL
	// the tenant serves is tenant knowledge, exactly like socket paths.
	Bridge string `json:"bridge,omitempty"`
	// contains filtered or unexported fields
}

AppRecord is one registered app in the hot registry.

type AuthGate added in v1.3.0

type AuthGate struct {
	Prefix string   `json:"prefix"`
	Allow  []string `json:"allow"`
}

AuthGate is one path-prefix allow list after normalization.

type AuthSettings added in v1.2.0

type AuthSettings struct {
	// Enabled turns the wall on or off for the site. Default: off.
	// Sites may override the global default; explicit off beats an
	// inherited on. Bare auth / auth on sets Enabled true without
	// Replace — inherit the global users and gates.
	Enabled *bool `json:"enabled,omitempty"`

	// Replace is true when a non-empty auth { … } block was parsed.
	// Site Replace configs supply the entire effective auth config
	// (users, gates, ttl) and do not merge with global.
	Replace bool `json:"replace,omitempty"`

	// Users is this level's credential table.
	Users []AuthUser `json:"users,omitempty"`

	// Gates is this level's path-prefix allow lists.
	Gates []AuthGate `json:"gates,omitempty"`

	// TTL is the sliding idle session timeout. Default: 8h.
	TTL *caddy.Duration `json:"ttl,omitempty"`
}

AuthSettings configures the site-scoped auth wall. It appears in the global janus options (default posture) and per site (override). A non-empty auth { … } block at the site replaces the global config wholesale. The full contract is docs/20260728-160734-capability-auth.md.

type AuthUser added in v1.2.0

type AuthUser struct {
	Name       string `json:"name"`
	Credential string `json:"credential"`
}

AuthUser is one cold-configured credential: a lowercased, header-safe username and its passhash blob.

type BrowseEntry added in v1.4.0

type BrowseEntry struct {
	Name         string
	URL          string
	RawURL       string
	Kind         string
	Icon         string
	Size         int64
	SizeText     string
	Modified     time.Time
	ModifiedText string
	PreviewURL   string
	Rendered     bool
}
type BrowseLink struct {
	Name string
	URL  string
}

type BrowsePage added in v1.4.0

type BrowsePage struct {
	Version     int
	Title       string
	Path        string
	RootName    string
	AssetBase   string
	Parent      *BrowseLink
	Breadcrumbs []BrowseLink
	Entries     []BrowseEntry
}

type BrowseRenderer added in v1.4.0

type BrowseRenderer struct {
	Extension   string          `json:"extension"`
	Command     []string        `json:"command"`
	ContentType string          `json:"content_type"`
	Timeout     *caddy.Duration `json:"timeout,omitempty"`
	MaxOutput   *int64          `json:"max_output,omitempty"`
	Concurrency *int            `json:"concurrency,omitempty"`
}

type BrowseRoot added in v1.4.0

type BrowseRoot struct {
	Path  string `json:"path"`
	Cache string `json:"cache,omitempty"`
}

type BrowseSettings added in v1.4.0

type BrowseSettings struct {
	Theme       string           `json:"theme,omitempty"`
	Timeout     *caddy.Duration  `json:"timeout,omitempty"`
	MaxOutput   *int64           `json:"max_output,omitempty"`
	Concurrency *int             `json:"concurrency,omitempty"`
	Renderers   []BrowseRenderer `json:"renderers,omitempty"`
	// contains filtered or unexported fields
}

type BrowseSiteSettings added in v1.4.0

type BrowseSiteSettings struct {
	Enabled *bool        `json:"enabled,omitempty"`
	Roots   []BrowseRoot `json:"roots,omitempty"`
}

type CacheSettings

type CacheSettings struct {
	// Enabled turns the cache on or off for the site. Default: off.
	// Sites may override the global default; explicit off beats an
	// inherited on.
	Enabled *bool `json:"enabled,omitempty"`

	// TTL is the freshness window for cached responses when the origin
	// sends no explicit lifetime. Default: 1s.
	TTL *caddy.Duration `json:"ttl,omitempty"`

	// TTLMax caps origin-declared lifetimes (s-maxage or max-age); a
	// larger declared value is clamped to this. Default: 10s.
	TTLMax *caddy.Duration `json:"ttl_max,omitempty"`

	// MaxBody is the largest response body, in bytes, the cache stores;
	// larger responses stream through uncached. Default: 262144 (256KiB).
	MaxBody *int64 `json:"max_body,omitempty"`

	// Debug adds an X-Janus-Cache header (HIT, MISS, BYPASS, …) to every
	// response served on the site. Default: off.
	Debug *bool `json:"debug,omitempty"`

	// MaxBytes is the process-wide memory pool shared by every site's
	// cache, in bytes. Global-only: illegal in a site block.
	// Default: 67108864 (64MiB).
	MaxBytes *int64 `json:"max_bytes,omitempty"`

	// MaxAppShare is the percentage of max_bytes one app may hold
	// (1–100). Global-only: illegal in a site block. Default: 50.
	MaxAppShare *int `json:"max_app_share,omitempty"`
}

CacheSettings configures the site-scoped micro-cache with request coalescing. It appears in the global janus options (default plus the process-wide pool knobs) and per site (override); unset keys cascade from the global settings, then built-in defaults. The full contract is docs/20260720-033201-capability-microcache.md.

type Control

type Control struct {
	// Mode is internal, local, or public.
	Mode string `json:"mode,omitempty"`

	// Listen is a unix path (internal) or http(s) URL (local/public).
	Listen string `json:"listen,omitempty"`

	// Token is the raw token:… suffix value (no "token:" prefix), if any.
	Token string `json:"token,omitempty"`

	// TokenKind is env, file, or literal.
	TokenKind string `json:"token_kind,omitempty"`

	// CertFile and KeyFile are the TLS certificate/key paths from the
	// cert:… and key:… arguments. Both or neither; only meaningful on a
	// TLS listener (public mode or an https:// listen). Unset, a TLS
	// listener uses the committed dev pair certs/ripdev.io.{crt,key}.
	CertFile string `json:"cert_file,omitempty"`
	KeyFile  string `json:"key_file,omitempty"`
	// contains filtered or unexported fields
}

Control is one self-contained control-plane listener.

type FilesPolicy added in v1.3.0

type FilesPolicy struct {
	Roots      []FilesRoot `json:"roots"`
	ProxyFirst []string    `json:"proxy_first,omitempty"`
	Shell      string      `json:"shell,omitempty"`
}

FilesPolicy declares ordered static roots, worker-first prefixes, and the independent SPA shell.

type FilesRoot added in v1.3.0

type FilesRoot struct {
	Path   string `json:"path"`
	Cache  string `json:"cache,omitempty"`
	Browse bool   `json:"browse"`
}

FilesRoot is one ordered root with a finite cache policy.

func (*FilesRoot) UnmarshalJSON added in v1.4.0

func (r *FilesRoot) UnmarshalJSON(data []byte) error

type Handler

type Handler struct {
	// Ping overrides the global ping default for this site when non-nil.
	Ping *bool `json:"ping,omitempty"`

	// Cache overrides the global cache default/tuning for this site when
	// non-nil (process-wide keys are illegal here).
	Cache *CacheSettings `json:"cache,omitempty"`

	// Hub overrides the global hub default/tuning for this site when
	// non-nil.
	Hub *HubSettings `json:"hub,omitempty"`

	// Auth overrides the global auth default for this site when non-nil.
	// A non-empty auth { … } block replaces the global config wholesale.
	Auth *AuthSettings `json:"auth,omitempty"`

	// Files overrides the global registered-file service default for this
	// site when non-nil.
	Files *bool `json:"files,omitempty"`

	// Browse overrides the global browse admission default and may carry
	// persistent cold roots for this exact-host site.
	Browse *BrowseSiteSettings `json:"browse,omitempty"`
	// contains filtered or unexported fields
}

Handler is the site-level data-plane admission module.

func (Handler) CaddyModule

func (Handler) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*Handler) Provision

func (h *Handler) Provision(ctx caddy.Context) error

Provision sets up the handler.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP handles admitted requests: on the plain-HTTP port with the mdns front door in shared mode, the handler is the front-door decider (mine serves the front door, not-mine passes through to the next route on the same server — the auto-HTTPS redirects — never 421); everywhere else, site-scoped /ping answers first when enabled and everything else routes through the data plane (registry hosts → upstreams; unknown hosts → 404).

func (*Handler) UnmarshalCaddyfile

func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler.

janus
janus {
    ping
    ping off
    cache off
    cache { ttl 5s; debug }
}

type HubSettings

type HubSettings struct {
	// Enabled turns the hub on or off for the site. Default: off.
	// Sites may override the global default; explicit off beats an
	// inherited on.
	Enabled *bool `json:"enabled,omitempty"`

	// Mode selects tenant-observed bridge admission or edge-only direct
	// admission. Default: bridge.
	Mode *string `json:"mode,omitempty"`

	// Path is the WebSocket endpoint path; only upgrade requests to it
	// are intercepted. Must start with "/" and contain no "?" or "#".
	// Default: "/hub".
	Path *string `json:"path,omitempty"`

	// MaxConns caps concurrent hub connections per app. An app spanning
	// several hub-enabled hosts is admitted against the minimum effective
	// value across all of them. Default: 4096.
	MaxConns *int `json:"max_conns,omitempty"`

	// MaxFrame is the largest client frame, in bytes; larger frames close
	// the connection (1009). Minimum: 1024 (1KiB). Default: 65536 (64KiB).
	MaxFrame *int64 `json:"max_frame,omitempty"`

	// MaxChannels caps channels one connection may join. Default: 128.
	MaxChannels *int `json:"max_channels,omitempty"`

	// Origin is the browser Origin policy: "same" (Origin host must
	// equal the request Host), "any" (no check — for token-authenticated
	// non-browser clients), or "same" plus allowlisted hostnames, or
	// hostnames alone. Failure answers 403 before any tenant contact.
	// Default: same.
	Origin []string `json:"origin,omitempty"`
}

HubSettings configures the site-scoped hub: per-app WebSocket fan-out terminated at the edge. It appears in the global janus options (default) and per site (override); unset keys cascade from the global settings, then built-in defaults. The full contract is docs/20260720-162350-hub-design.md.

type MdnsSettings added in v1.1.0

type MdnsSettings struct {
	// Name is the advertised mDNS name: exactly one label plus ".local".
	// Default: "janus.local".
	Name string `json:"name,omitempty"`

	// Canonical is the https:// origin the front door hands off to
	// (client-side probe + redirect, diagnostic mode on failure).
	// Origin only — no path, query, fragment, or userinfo; never an IP
	// literal; never a .local name. Default: unset (plain dashboard).
	Canonical string `json:"canonical,omitempty"`

	// Interfaces pins advertising to exactly these interfaces. Default:
	// unset — the live multicast interface set with the loopback and
	// IPv4 link-local block list applied.
	Interfaces []string `json:"interface,omitempty"`

	// Apps controls per-app `.local` advertising. Default: on.
	Apps *bool `json:"apps,omitempty"`

	// Listen selects the front door's mode. Unset (the default) is
	// shared mode: the front door rides inside the HTTP app's plain-HTTP
	// port server behind a normal site block (http://*.local { janus })
	// and the janus site handler decides per request. Set ("[host]:port")
	// is dedicated mode: Janus opens its own listener at that address
	// with the strict Host allowlist.
	Listen string `json:"listen,omitempty"`
	// contains filtered or unexported fields
}

MdnsSettings configures the process-wide mdns capability: the advertised `.local` identity, per-app advertising, and the plain-HTTP front door. The full contract is docs/20260722-034619-capability-mdns.md.

type SitePolicy added in v1.3.0

type SitePolicy struct {
	Host    string            `json:"host"`
	Dir     string            `json:"dir"`
	Aliases map[string]string `json:"aliases,omitempty"`
}

SitePolicy declares one directory-gated host pattern and its exact aliases.

type Upstream

type Upstream struct {
	// Path is the unix socket path Janus may dial.
	Path string `json:"path"`

	// Doorbell marks the tenant's wake-up socket. A doorbell entry must
	// be the only entry in the list. Phase 3 stores and validates the
	// flag; ringing is data-plane behavior (Phase 4).
	Doorbell bool `json:"doorbell,omitempty"`
}

Upstream is one entry in an app's upstream list.

Directories

Path Synopsis
testkit is the acceptance-suite support binary for test.sh: fixture HTTP servers on unix sockets, an RFC 6455 WebSocket driver, and small shell utilities.
testkit is the acceptance-suite support binary for test.sh: fixture HTTP servers on unix sockets, an RFC 6455 WebSocket driver, and small shell utilities.

Jump to

Keyboard shortcuts

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