# Rules & Descriptors

- Canonical URL: https://docs.fairvisor.com/docs/policy/rules/
- Section: docs
- Last updated: n/a
> Rule structure, descriptor keys, and algorithm_config reference.


Rules are the heart of a policy. Each rule specifies which requests it applies to (`limit_keys`, `match`), and which algorithm enforces the limit.

## Rule object

```json
{
  "name": "per-org-rps",
  "limit_keys": ["jwt:org_id"],
  "algorithm": "token_bucket",
  "algorithm_config": {
    "tokens_per_second": 100,
    "burst": 200
  },
  "match": {
    "jwt:plan": "enterprise"
  }
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | **yes** | Non-empty. Used in counter keys, log lines, response headers, and metrics labels. Must be unique within the policy. |
| `limit_keys` | array | **yes** | One or more [descriptor keys](#descriptor-keys) that define the partitioning dimension. Each unique combination of values gets its own counter. |
| `algorithm` | string | **yes** | One of `token_bucket`, `cost_based`, `token_bucket_llm`. |
| `algorithm_config` | object | **yes** | Algorithm-specific configuration (see below). |
| `match` | object | no | Key/value equality filters. The rule is only evaluated if **all** match conditions are satisfied. |

## Rule evaluation order

Rules in the `rules[]` array are evaluated **in order, all-match**: every rule
whose `match` conditions are satisfied is checked. Evaluation stops at the first
rule that **rejects** the request. A rule that matches but does not reject
continues to the next rule.

Example with three rules:

| Rule | match | result | next rule? |
|---|---|---|---|
| `enterprise` | jwt:plan = enterprise | allow | yes |
| `per-org-rps` | (no match condition) | allow | yes |
| `free-tier-cap` | (no match condition) | **reject** | — |

The `fallback_limit` is applied only when **no** rule in `rules[]` matches
(all `match` conditions failed for every rule).

## Descriptor keys

Descriptor keys identify where a value is extracted from the request. They have the format `source:name`.

### `jwt:<claim>`

Extract a claim from the JWT payload in the `Authorization: Bearer <token>` header.

```json
"limit_keys": ["jwt:org_id"]
```

<div class="callout callout-warning">
<span class="callout-icon">⚠️</span>
<p><strong>JWT signatures are not verified.</strong> Claims are decoded for value extraction only. Fairvisor trusts that your gateway (nginx, Envoy, Kong, etc.) has already validated the token before forwarding the request. Never rely on Fairvisor alone for JWT authentication.</p>
</div>

- Claim names must match `[A-Za-z0-9_-]+`
- If the header is absent or not a valid JWT, the descriptor value is nil and the rule is skipped

### `header:<name>`

Extract an HTTP request header value.

```json
"limit_keys": ["header:x-api-key"]
```

Header matching is case-insensitive and normalises both `-` and `_` separators. All of these resolve `x-api-key`:
- `X-API-Key`
- `x-api-key`
- `X_API_KEY`

### `query:<name>`

Extract a URL query parameter.

```json
"limit_keys": ["query:tenant_id"]
```

### `ip:address`

The client IP address from `ngx.var.remote_addr`.

```json
"limit_keys": ["ip:address"]
```

### `ip:country`

GeoIP country code (two-letter ISO 3166-1 alpha-2). Requires a GeoIP module in OpenResty.

```json
"limit_keys": ["ip:country"]
```

### `ip:asn`

GeoIP Autonomous System Number.

### `ip:type`

Network type descriptor derived from ASN/type mapping (`isp`, `hosting`, `business`, `education_research`, `government_admin`, `unrouted`, `unknown`).

See [ip:type](/docs/policy/ip-type/).

### `ip:tor`

Boolean selector for Tor exit classification.

- Primary source: nginx `geo` variable (`$is_tor_exit`) generated from Tor exit list include.
- Optional override source: `X-Tor-Exit` header (`1/0`, `true/false`, `yes/no`).
- Normalized values used by policy matching: `"true"` / `"false"`.

```json
"limit_keys": ["ip:tor"]
```

### `ua:bot`

Bot detection based on User-Agent. Returns the string `"true"` or `"false"`.

```json
"limit_keys": ["ua:bot"]
```

Known bot User-Agents include major search, AI crawler, assistant-user, social preview, and SEO crawler families.

Detection is case-insensitive substring matching with a generated runtime automaton.

### `ua:bot_category`

Bot category for matching/partitioning. See [ua:bot_category values](/docs/policy/bot-categories/) for the full list of categories and examples.

## Composite keys (multi-dimensional limiting)

Provide multiple descriptor keys to create a composite partition. Each unique combination of values gets its own counter bucket.

```json
"limit_keys": ["jwt:org_id", "jwt:user_id"]
```

This limits each `(org_id, user_id)` pair independently. An org with 1000 users gets 1000 separate buckets.

The composite key is formed by joining descriptor values with `|`:

```
org-abc|user-xyz
```

## `match` conditions

The `match` object applies additional filters **before** the rule's limiter is invoked. Each key/value pair must match the corresponding descriptor exactly.

```json
{
  "name": "enterprise-limit",
  "limit_keys": ["jwt:org_id"],
  "algorithm": "token_bucket",
  "algorithm_config": { "tokens_per_second": 1000, "burst": 2000 },
  "match": {
    "jwt:plan": "enterprise"
  }
}
```

This rule only applies to requests where the JWT contains `"plan": "enterprise"`. Other plans fall through to the next rule or the `fallback_limit`.

## `fallback_limit`

The `fallback_limit` is applied when **no** rule in the policy matches a request (all `match` conditions failed). It has the same structure as a rule object, without a `name` requirement:

```json
{
  "spec": {
    "selector": { "pathPrefix": "/api/" },
    "rules": [
      {
        "name": "enterprise",
        "limit_keys": ["jwt:org_id"],
        "algorithm": "token_bucket",
        "algorithm_config": { "tokens_per_second": 1000, "burst": 2000 },
        "match": { "jwt:plan": "enterprise" }
      }
    ],
    "fallback_limit": {
      "name": "free-tier",
      "limit_keys": ["jwt:org_id"],
      "algorithm": "token_bucket",
      "algorithm_config": { "tokens_per_second": 10, "burst": 20 }
    }
  }
}
```

## Missing descriptors

If a descriptor key cannot be resolved (e.g., the `Authorization` header is absent when using `jwt:org_id`), the rule is **skipped** with a warning log and a `fairvisor_descriptor_missing_total` metric increment. It is **not** an error.

To enforce that a value must be present, use `ua:bot` in combination with a separate kill switch on the bot UA, or add an `ip:address` rule as a universal fallback.

## Algorithm configs

| Algorithm | When to use | Key fields |
|---|---|---|
| `token_bucket` | Request-rate limiting (RPS) | `tokens_per_second`, `burst` |
| `cost_based` | Period spend quotas | `budget`, `period`, `staged_actions` |
| `token_bucket_llm` | LLM token throughput | `tokens_per_minute`, `tokens_per_day` |

See the [Algorithms](/docs/algorithms/token-bucket/) section for full field references.

