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:

  • reason is required, non-empty, max 256 chars
  • expires_at is 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