Bundle Structure
A policy bundle is a single JSON file that defines all enforcement rules for an edge instance. The file is loaded on startup and can be hot-reloaded without restarting the process.
Top-level fields
{
"bundle_version": 1,
"issued_at": "2026-01-01T00:00:00Z",
"expires_at": "2030-01-01T00:00:00Z",
"global_shadow": { ... },
"kill_switch_override": { ... },
"policies": [ ... ],
"kill_switches": [ ... ],
"defaults": {}
}
| Field | Type | Required | Description |
|---|---|---|---|
bundle_version |
integer | yes | Monotonic counter. Must be > 0. Hot-reload only applies a new bundle if this value is strictly greater than the currently loaded version. |
issued_at |
string | no | ISO 8601 UTC timestamp, e.g. "2026-01-15T10:00:00Z". Informational only. |
expires_at |
string | no | ISO 8601 UTC. The bundle is rejected at load time if the current clock is past this timestamp. An already-loaded bundle continues to run normally after its expiry passes — there is no per-request re-check. |
global_shadow |
object | no | Runtime override block. When active (enabled=true and not expired), all policies are treated as shadow mode at runtime. |
kill_switch_override |
object | no | Runtime override block. When active, kill-switch checks are skipped. |
policies |
array | yes | Array of policy objects. At least one required. |
kill_switches |
array | no | Array of kill switch entries. Evaluated before all policy rules. |
defaults |
object | no | Free-form defaults object; passed through without validation. |
Runtime override blocks
Both override blocks are optional and validated at bundle load time.
{
"global_shadow": {
"enabled": true,
"reason": "incident-2026-02-20",
"expires_at": "2026-02-20T19:00:00Z"
},
"kill_switch_override": {
"enabled": true,
"reason": "incident-2026-02-20",
"expires_at": "2026-02-20T19:00:00Z"
}
}
Validation rules when enabled=true:
reasonis required, non-empty, max 256 charsexpires_atis required, ISO 8601 UTC, and must be in the future at load time
Runtime behavior:
global_shadow: converts matched policy outcomes to shadow semantics (client path is allow; calculations still run)kill_switch_override: bypasses kill-switch pre-check- No extra client response headers are added for these modes; observability is via logs and metrics
Monotonic versioning
Each time you update a bundle you must increment bundle_version. The hot-reload timer will silently skip the file if the version is not higher than the current one, logging version_not_monotonic at debug level.
# Safe incremental update
jq '.bundle_version += 1' policy.json > policy.json.new && mv policy.json.new policy.json
Policy object
{
"id": "my-api-limits",
"spec": { ... }
}
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Unique identifier across all policies in the bundle. Used in log lines, metrics labels, and response headers. Must be non-empty. |
spec |
object | yes | Policy specification (see below). |
Policy spec
{
"selector": { ... },
"mode": "enforce",
"rules": [ ... ],
"fallback_limit": { ... },
"loop_detection": { ... },
"circuit_breaker": { ... }
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
selector |
object | yes | — | Route matching definition. See Selectors. |
mode |
string | no | "enforce" |
"enforce" or "shadow". See Shadow Mode. |
rules |
array | yes | — | Array of rule objects. |
fallback_limit |
object | no | — | Rule-like object applied when no rule matches a request. Same structure as a rule. |
loop_detection |
object | no | — | See Loop Detection. |
circuit_breaker |
object | no | — | See Circuit Breaker. |
Minimal example
One policy applied to all /api/v1/ routes. Every unique IP address gets its own token bucket: up to 100 requests per second steady-state, with a burst allowance of 200. Requests beyond that are rejected with 429.
{
"bundle_version": 1,
"policies": [
{
"id": "api-v1",
"spec": {
"selector": { "pathPrefix": "/api/v1/" },
"rules": [
{
"name": "global-rps",
"limit_keys": ["ip:address"],
"algorithm": "token_bucket",
"algorithm_config": {
"tokens_per_second": 100,
"burst": 200
}
}
]
}
}
],
"kill_switches": []
}
Bundle signing (optional)
When FAIRVISOR_BUNDLE_SIGNING_KEY is set, the edge verifies an HMAC-SHA256 signature prepended to the bundle file. The signature is placed on the first line as a base64-encoded string, followed by a newline, followed by the JSON payload:
<base64-hmac-sha256-signature>
{"bundle_version":1, ...}
This prevents loading a tampered or accidentally overwritten bundle. The constant-time comparison prevents timing attacks.
Hot reload
Policy bundles are reloaded on a configurable interval (FAIRVISOR_CONFIG_POLL_INTERVAL, default 30 s). The edge re-reads the file and applies the bundle only if bundle_version has increased. No traffic is dropped during reload.
Load-time check only. expires_at is validated when the bundle is loaded. A bundle that is already running continues to run after its expiry timestamp passes — there is no per-request re-check. To retire an expired bundle, push a replacement with a higher bundle_version. The global_shadow and kill_switch_override blocks each have their own expires_at that is evaluated on every request.
To force an immediate reload via a running SaaS-connected edge:
fairvisor status # confirm current version
# Then push a new bundle via the SaaS dashboard