Doorman
DocsRule BuilderGet StartedGitHub
Doorman
DocsRule Builder

© 2026 griffen.codes

    Documentation

    Getting Started

    • Getting Started

    Configuration

    • Configuration
    • Templates
    • Examples

    Commands

    • Commands Overview

    Guides

    • CI/CD Integration
    • Vercel Setup
    • Cloudflare Setup
    • Cloudflare Migration
    • Fastly Setup
    • Fastly Migration
    • GCP Setup
    View on GitHub Wiki
    Docs/Configuration/Configuration
    Configuration

    Configuration

    Learn about the JSON configuration file format, rules, conditions, and actions.

    Edit on GitHub

    Doorman uses a JSON configuration file to define your firewall rules. The configuration is validated using JSON Schema and provides full TypeScript support.

    Schema URL

    Add the schema reference to your config file for editor autocompletion and validation:

    json
    {
      "$schema": "https://doorman.griffen.codes/schema.json"
    }

    Two Rule Formats

    Doorman recognizes two on-disk config shapes, auto-detected by whether the file has a top-level provider or providers field:

    • Unified format — any config with provider and/or providers set. Required for Cloudflare and Fastly. Rules use conditions/enabled/flat action: { type }. This is the format the rest of this page documents.
    • Legacy format — a Vercel-only config with no provider/providers field at all. Rules use conditionGroup/active/action: { mitigate }. Kept for backward compatibility with configs written before multi-provider support existed. See Legacy (Vercel-only) Rule Format below.

    These are genuinely different shapes, not a relabeling of the same fields — mixing them in one rule (e.g. conditionGroup inside a provider-tagged config) fails validation. If you're setting up Cloudflare or Fastly, use the unified format from the start.

    Basic Structure

    Vercel Configuration (legacy format)

    json
    {
      "$schema": "https://doorman.griffen.codes/schema.json",
      "projectId": "prj_abc123",
      "teamId": "team_xyz789",
      "rules": [],
      "ips": []
    }

    Cloudflare Configuration (unified format)

    json
    {
      "$schema": "https://doorman.griffen.codes/schema.json",
      "provider": "cloudflare",
      "providers": {
        "cloudflare": {
          "zoneId": "zone_abc123",
          "accountId": "acc_xyz789"
        }
      },
      "rules": [],
      "ips": []
    }

    Fastly Configuration (unified format)

    json
    {
      "$schema": "https://doorman.griffen.codes/schema.json",
      "provider": "fastly",
      "providers": {
        "fastly": {
          "workspaceId": "workspace_abc123"
        }
      },
      "rules": [],
      "ips": []
    }

    Multi-Provider Configuration (unified format)

    json
    {
      "$schema": "https://doorman.griffen.codes/schema.json",
      "provider": "cloudflare",
      "providers": {
        "vercel": {
          "projectId": "prj_abc123",
          "teamId": "team_xyz789"
        },
        "cloudflare": {
          "zoneId": "zone_abc123",
          "accountId": "acc_xyz789"
        },
        "fastly": {
          "workspaceId": "workspace_abc123"
        }
      },
      "rules": [],
      "ips": []
    }

    Root Properties

    PropertyTypeRequiredDescription
    $schemastringNoJSON Schema URL for validation
    providerstringNoDefault provider ("vercel", "cloudflare", or "fastly"). Presence of this field (or providers) switches the file into unified format.
    projectIdstringVercel OnlyVercel project ID (legacy format)
    teamIdstringVercel OnlyVercel team ID (legacy format, optional)
    providersobjectMulti-ProviderProvider-specific configurations
    rulesarrayYesArray of firewall rules — shape depends on format, see above
    ipsarrayNoArray of IP blocking rules
    managedRulesarrayNoCloudflare only. Vendor-managed rulesets (e.g. Cloudflare Managed Ruleset, OWASP CRS) to deploy — see Managed Rule Groups
    versionnumberNoConfiguration version
    firewallEnabledbooleanNoEnable/disable firewall

    Rules (Unified Format)

    Use this format for Cloudflare, Fastly, or any config with provider/providers set — including a Vercel config you've explicitly tagged with "provider": "vercel".

    Rule Structure

    json
    {
      "id": "rule_block_bots",
      "name": "Block Bad Bots",
      "description": "Block malicious bots and crawlers",
      "enabled": true,
      "conditions": [
        { "field": "user_agent", "operator": "contains", "value": "bot" }
      ],
      "action": { "type": "deny" }
    }

    Rule Properties

    PropertyTypeRequiredDescription
    idstringNoUnique rule identifier
    namestringYesHuman-readable rule name
    descriptionstringNoRule description
    enabledbooleanYesWhether rule is enabled
    conditionsarrayYesArray of conditions (at least one required) — see below
    conditionLogicstringNo"AND" (default) or "OR", applied across all conditions when none of them set group — see Grouping Conditions
    actionobjectYesAction to take when conditions match
    prioritynumberNoEvaluation order — lower runs first. Fully honoured on Cloudflare; best-effort on Vercel, which can't reposition rules that already exist remotely.

    Conditions

    Each condition has a field, operator, and (usually) a value:

    json
    { "field": "path", "operator": "starts_with", "value": "/admin" }

    exists/not_exists operators carry no value — everything else requires one. header/query/cookie conditions take an additional key to scope to a specific name:

    json
    { "field": "header", "key": "X-Custom-Header", "operator": "eq", "value": "expected" }

    Cloudflare caveat: key is only honoured for header and cookie today — a keyed query condition currently matches against the whole query string instead of the named parameter. Tracked in #263.

    Grouping Conditions

    For a simple rule, omit group entirely — all conditions implicitly share group 0 and are AND'd together (or OR'd, if you set conditionLogic: "OR").

    For "OR of AND-groups" logic (e.g. (path starts with "/admin" AND method is POST) OR (IP is 192.168.1.1)), tag each condition with a group number — conditions sharing a group are AND'd, and distinct group values are OR'd against each other. group takes priority over conditionLogic the moment any condition sets it:

    json
    {
      "conditions": [
        { "field": "path", "operator": "starts_with", "value": "/admin", "group": 0 },
        { "field": "method", "operator": "eq", "value": "POST", "group": 0 },
        { "field": "ip", "operator": "eq", "value": "192.168.1.1", "group": 1 }
      ]
    }

    Field Support

    FieldVercelCloudflareFastly
    ip✅✅✅
    country✅✅✅
    region✅✅❌ dropped, warned
    city✅✅❌ dropped, warned
    asn✅✅❌ dropped, warned
    path✅✅✅
    host✅✅✅
    method✅✅✅
    header✅✅ requires key⚠️ requires key, else dropped/warned
    query✅✅ key optional — scopes to one parameter when set⚠️ requires key, else dropped/warned
    cookie✅✅ requires key⚠️ requires key, else dropped/warned
    user_agent✅✅✅
    referer❌ dropped, warned✅❌ dropped, warned
    scheme✅✅✅
    port❌ dropped, warned✅❌ dropped, warned

    exists/not_exists on Fastly only work on the keyed fields (header/query/cookie) — unsupported (dropped, warned) on every other Fastly field.

    Operator Support

    OperatorVercelCloudflareFastly
    eq✅✅✅
    ne✅ maps to eq + neg: true✅✅
    contains✅✅✅
    not_contains✅ maps to sub + neg: true✅✅
    starts_with✅✅⚠️ wildcard approximation, not a true prefix match
    ends_with✅✅⚠️ wildcard approximation, not a true suffix match (identical mechanism to starts_with)
    matches (regex)✅✅⚠️ supported, but regex syntax may need adjustment — always warned
    in✅✅✅
    not_in✅ maps to inc + neg: true✅✅
    gt⚠️ no numeric-comparison operator exists — dropped, warned✅⚠️ boundary-inclusive approximation (greater_equal)
    ge⚠️ no numeric-comparison operator exists — dropped, warned✅✅
    lt⚠️ no numeric-comparison operator exists — dropped, warned✅⚠️ boundary-inclusive approximation (lesser_equal)
    le⚠️ no numeric-comparison operator exists — dropped, warned✅✅
    exists✅✅⚠️ only on keyed fields (header/query/cookie)
    not_exists✅✅⚠️ only on keyed fields (header/query/cookie)

    ne/not_contains/not_in are fully supported on Vercel via its positive-operator-plus-neg-flag model (the same mechanism not_exists's nex already uses) — #261 fixed these three, which previously mis-mapped silently to plain eq. gt/ge/lt/le remain a genuine platform gap, not a bug: Vercel's operator vocabulary has no numeric-comparison concept at all, so a condition using one of these four is dropped with a warning rather than synced incorrectly.

    Action Support

    ActionVercelCloudflareFastly
    log✅✅⚠️ no dedicated action, maps to allow (request logging is separately always-on)
    deny✅✅✅
    challenge✅✅✅
    bypass✅✅⚠️ no equivalent, maps to allow
    rate_limit⚠️ silently becomes a plain rule with no rate-limit effect if rateLimit is omitted⚠️ silently becomes a plain block rule with no rate-limit effect if rateLimit is omitted✅ throws if rateLimit is missing; requires a pre-existing Fastly Signal named doorman-rate-limit-<ruleId> that doorman does not create for you
    redirect⚠️ silently becomes a rule with no redirect target if redirect is omitted; statusCode/preserveQueryString are silently dropped even when present⚠️ silently becomes a rule with no redirect target if redirect is omitted✅ falls back to allow (warned) if redirect is missing, otherwise fully supported
    allow✅ mapped to bypass (Vercel has no native allow)✅✅
    block✅ mapped to deny (Vercel has no native block)✅ (same as deny)✅ (same as deny)

    #262 fixed allow/block reaching Vercel's API as an invalid native action value — both are now automatically mapped to their nearest Vercel equivalent, so either spelling works in a Vercel-targeted config.

    Rate Limiting

    json
    {
      "action": {
        "type": "rate_limit",
        "rateLimit": {
          "requests": 100,
          "window": "60s",
          "characteristics": ["ip.src"]
        }
      }
    }

    window accepts a number followed by s/m/h/d.

    Redirect

    json
    {
      "action": {
        "type": "redirect",
        "redirect": {
          "location": "https://example.com/blocked",
          "statusCode": 302,
          "permanent": false,
          "preserveQueryString": false
        }
      }
    }

    location accepts an absolute URL or a path starting with /. On Vercel, only location/permanent are actually used — statusCode/preserveQueryString are silently dropped, so omit them if you need this rule to round-trip cleanly to Vercel.

    Legacy (Vercel-only) Rule Format

    Only applies to a config with no provider/providers field — i.e. the original Vercel-only shape shown in Basic Structure above.

    Rule Structure

    json
    {
      "id": "rule_block_bots",
      "name": "Block Bad Bots",
      "description": "Block malicious bots and crawlers",
      "active": true,
      "conditionGroup": [
        {
          "conditions": [
            { "type": "user_agent", "op": "sub", "value": "bot", "neg": false }
          ]
        }
      ],
      "action": {
        "mitigate": { "action": "deny" }
      }
    }

    Rule Properties

    PropertyTypeRequiredDescription
    idstringNoUnique rule identifier
    namestringYesHuman-readable rule name
    descriptionstringNoRule description
    activebooleanYesWhether rule is enabled
    conditionGrouparrayYesArray of condition groups — OR logic between groups, AND logic within a group
    actionobjectYes{ "mitigate": { "action": ... } } — action to take when conditions match
    json
    {
      "conditionGroup": [
        {
          "conditions": [
            { "type": "path", "op": "pre", "value": "/admin" },
            { "type": "method", "op": "eq", "value": "POST" }
          ]
        },
        {
          "conditions": [
            { "type": "ip_address", "op": "eq", "value": "192.168.1.1" }
          ]
        }
      ]
    }

    This translates to: (path starts with "/admin" AND method equals "POST") OR (IP equals "192.168.1.1").

    Legacy Condition Types

    host, path, method, header, query, cookie, target_path, ip_address, region, protocol, scheme, environment, user_agent, geo_continent, geo_country, geo_country_region, geo_city, geo_as_number, ja4_digest, ja3_digest, rate_limit_api_id

    header/cookie require a key. For ip_address/method/environment/protocol, op must be eq or inc — no other operator is valid for these four types.

    Legacy Operators

    OperatorMeaning
    eqEquals
    preStarts with
    sufEnds with
    subContains
    incIs any of (array) — requires value to be an array
    reRegex match
    exExists
    nexDoes not exist

    neg: true negates a condition, but can't be combined with ex/nex — use nex directly instead of neg: true + ex.

    Legacy Actions

    log, deny, challenge, bypass, rate_limit, redirect — this is a closed set; allow/block are not valid here (use bypass/deny).

    json
    { "action": { "mitigate": { "action": "rate_limit", "rateLimit": { "requests": 100, "window": "60s" } } } }
    json
    { "action": { "mitigate": { "action": "redirect", "redirect": { "location": "/correct-path", "permanent": false } } } }

    IP Blocking Rules

    json
    {
      "ips": [
        {
          "id": "ip_block_suspicious",
          "ip": "192.168.1.100/32",
          "hostname": "suspicious-host",
          "action": "deny",
          "notes": "Blocked due to suspicious activity"
        }
      ]
    }
    PropertyTypeRequiredDescription
    ipstringYesIP address or CIDR range
    hostnamestringNoHostname for documentation
    actionstringYes"deny" in both formats; "allow" is also valid in the unified format
    notesstringNoNotes about the block

    Same shape in both the legacy and unified formats.

    Managed Rule Groups

    Cloudflare only. Deploy a vendor-managed ruleset (Cloudflare Managed Ruleset, OWASP CRS, etc.) alongside your custom rules, with optional overrides — instead of hand-writing rules to replicate what a preconfigured WAF ruleset already covers.

    json
    {
      "managedRules": [
        {
          "id": "execute-owasp-crs",
          "ruleset": "efb7b8c949ac4650a09736fc376e9aee",
          "name": "OWASP Core Ruleset",
          "enabled": true,
          "action": "log",
          "overrides": [
            { "ruleId": "981176", "action": "deny" },
            { "ruleId": "981245", "enabled": false }
          ]
        }
      ]
    }
    PropertyTypeRequiredDescription
    idstringNoDoorman's diff/sync identifier for this deployment. Omit for a new declaration; Doorman assigns one on first sync.
    rulesetstringYesThe vendor ruleset id to deploy (e.g. Cloudflare Managed Ruleset's well-known id shown above)
    namestringNoHuman label
    enabledbooleanYesWhether this deployment is active
    actionstringNoRuleset-wide override — downgrade every rule in the group to this action. One of log, deny, challenge, allow
    overridesarrayNoPer-rule overrides within the ruleset — { "ruleId": string, "action"?: string, "enabled"?: boolean }, referenced by the vendor's rule id within that ruleset, not a Doorman id

    managedRules needs to live alongside a provider: "cloudflare"/providers.cloudflare block — it's part of the unified format, same as rules/ips.

    Adding this to a provider/providers-tagged config that already has custom rules works alongside them — managed rulesets deploy in Cloudflare's separate managed-rules phase, evaluated independently of your custom rules, so there's no ordering interaction to think about between the two.

    Environment Variables

    Vercel

    bash
    VERCEL_TOKEN="your_vercel_token"
    VERCEL_PROJECT_ID="prj_abc123"
    VERCEL_TEAM_ID="team_xyz789"

    Cloudflare

    bash
    CLOUDFLARE_API_TOKEN="your_api_token"
    CLOUDFLARE_ZONE_ID="zone_abc123"
    CLOUDFLARE_ACCOUNT_ID="acc_xyz789"

    Fastly

    bash
    FASTLY_API_TOKEN="your_api_token"
    FASTLY_WORKSPACE_ID="workspace_abc123"

    Provider Selection

    bash
    DOORMAN_PROVIDER="cloudflare"  # or "vercel" or "fastly"

    Best Practices

    1. Use descriptive names for rules and IPs
    2. Add descriptions to explain rule purposes
    3. Group related conditions logically
    4. Use CIDR notation for IP ranges
    5. Order rules by frequency (most common first)
    6. Test rules in staging before production
    7. Start with log actions before blocking
    8. Keep backups of working configurations
    9. Avoid the operators/actions flagged 🚨 above for whichever provider you're targeting, until their tracking issues are fixed

    Related Pages

    • Getting Started — Quick setup guide
    • Commands Overview — CLI command reference
    • Examples — Real-world configuration examples
    • Cloudflare Setup — Cloudflare-specific setup, including managed rule groups

    This content is sourced from the GitHub Wiki.

    PreviousGetting StartedNextTemplates