Docs

Guards

Guards Guards protect your routes with declarative rules — no code required. Configure them through the admin UI or API to block IPs, rate-limit requests, and control access. How Guards Work Guards run automatically at two points in the request lifecycle: 1. Route Detection 2. Pr

Guards

Guards protect your routes with declarative rules — no code required. Configure them through the admin UI or API to block IPs, rate-limit requests, and control access.

How Guards Work

Guards run automatically at two points in the request lifecycle:

1. Route Detection
2. Pre-Auth Guards          <-- IP blocking, rate limiting (before login)
3. JWT Authentication
4. Role/Permission Check
5. Post-Auth Guards         <-- User-specific rate limiting (after login)
6. Pre-Hooks  Handler  Post-Hooks
  • Pre-auth guards run before login. Use for IP-based rules and global rate limiting.
  • Post-auth guards run after login. Use for user-specific rules.

Rule Types

Rule Type Description Config
ip_whitelist Only allow listed IPs (blocks everyone else) {"ips": ["1.2.3.4", "10.0.0.0/8"]}
ip_blacklist Block listed IPs (allows everyone else) {"ips": ["5.6.7.8"]}
rate_limit_by_ip Rate limit per client IP per route {"maxRequests": 100, "perSeconds": 60}
rate_limit_by_user Rate limit per user per route (post-auth only) {"maxRequests": 50, "perSeconds": 60}
rate_limit_by_route Rate limit per route (all users share the limit) {"maxRequests": 200, "perSeconds": 60}

IP rules support CIDR notation: 10.0.0.0/8, 192.168.1.0/24, 172.16.0.0/12.

When a rate limit is hit, the response returns 429 with structured headers (see Rejection Response below).

Guard Tree

Guards can be combined using AND / OR logic to build complex rules.

AND (all rules must pass)

AND Guard
├── ip_whitelist: only office IPs
└── rate_limit_by_ip: max 100/min

Both conditions must pass. If either fails, the request is rejected.

OR (at least one rule must pass)

OR Guard
├── ip_whitelist: allow internal IPs (pass immediately)
└── rate_limit_by_ip: max 100/min (checked only if IP not whitelisted)

Internal IPs bypass rate limiting. External IPs get rate-limited.

Nesting

You can nest guards for more complex logic:

OR Guard (root)
├── ip_whitelist: 10.0.0.0/8
└── AND Guard (child)
    ├── rate_limit_by_ip: max 100/min
    └── rate_limit_by_user: max 50/min

Result: Internal IPs pass immediately. External IPs must pass both rate limits.

Configuration

Create a Guard

POST /api/enfyra_guard
{
  "name": "Login Rate Limit",
  "position": "pre_auth",
  "combinator": "and",
  "isEnabled": true,
  "route": { "id": "<route_id>" },
  "methods": [
    { "id": "<POST_method_id>" }
  ]
}
Field Values Description
position pre_auth, post_auth When the guard runs
combinator and, or How rules are combined (default: and)
isGlobal true / false Apply to all routes
route relation Apply to a specific route (null if global)
methods relation Apply to specific HTTP methods (empty = all methods)
priority number Execution order (lower = first)

Add Rules to a Guard

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_ip",
  "config": { "maxRequests": 5, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" }
}

To scope a rule to specific users only:

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_user",
  "config": { "maxRequests": 30, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" },
  "users": [
    { "id": "<user_id_1>" },
    { "id": "<user_id_2>" }
  ]
}

When users is empty, the rule applies to everyone.

Nest Guards

Set parent to create a child guard:

POST /api/enfyra_guard
{
  "name": "External Rate Limit",
  "position": "pre_auth",
  "combinator": "and",
  "parent": { "id": "<parent_guard_id>" },
  "isEnabled": true
}

Common Patterns

Block Bad IPs from All Routes

POST /api/enfyra_guard
{
  "name": "Global IP Blacklist",
  "position": "pre_auth",
  "combinator": "and",
  "isGlobal": true,
  "isEnabled": true
}

POST /api/enfyra_guard_rule
{
  "type": "ip_blacklist",
  "config": { "ips": ["1.2.3.4", "5.6.7.8"] },
  "guard": { "id": "<guard_id>" }
}

Rate Limit Login Attempts

5 requests per minute per IP, before authentication:

POST /api/enfyra_guard
{
  "name": "Login Rate Limit",
  "position": "pre_auth",
  "combinator": "and",
  "isEnabled": true,
  "route": { "id": "<login_route_id>" },
  "methods": [{ "id": "<POST_method_id>" }]
}

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_ip",
  "config": { "maxRequests": 5, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" }
}

Office IP Only + Rate Limit for Admin Routes

POST /api/enfyra_guard
{
  "name": "Admin Access Control",
  "position": "post_auth",
  "combinator": "and",
  "isEnabled": true,
  "route": { "id": "<admin_route_id>" }
}

POST /api/enfyra_guard_rule
{
  "type": "ip_whitelist",
  "config": { "ips": ["10.0.0.0/8", "203.0.113.0/24"] },
  "guard": { "id": "<guard_id>" }
}

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_user",
  "config": { "maxRequests": 100, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" }
}

Whitelist OR Rate Limit (Internal IPs bypass limits)

POST /api/enfyra_guard
{
  "name": "Internal or Rate Limited",
  "position": "pre_auth",
  "combinator": "or",
  "isGlobal": true,
  "isEnabled": true
}

POST /api/enfyra_guard_rule
{
  "type": "ip_whitelist",
  "config": { "ips": ["10.0.0.0/8"] },
  "guard": { "id": "<guard_id>" }
}

// Create child guard for external traffic
POST /api/enfyra_guard
{
  "name": "External Rate Limit",
  "position": "pre_auth",
  "combinator": "and",
  "parent": { "id": "<root_guard_id>" },
  "isEnabled": true
}

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_ip",
  "config": { "maxRequests": 100, "perSeconds": 60 },
  "guard": { "id": "<child_guard_id>" }
}

Global API Rate Limit

POST /api/enfyra_guard
{
  "name": "Global API Rate Limit",
  "position": "pre_auth",
  "combinator": "and",
  "isGlobal": true,
  "isEnabled": true
}

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_ip",
  "config": { "maxRequests": 200, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" }
}

User-Specific Stricter Limits

Apply stricter limits to specific users:

POST /api/enfyra_guard
{
  "name": "Heavy User Limit",
  "position": "post_auth",
  "combinator": "and",
  "isEnabled": true,
  "route": { "id": "<api_route_id>" }
}

POST /api/enfyra_guard_rule
{
  "type": "rate_limit_by_user",
  "config": { "maxRequests": 30, "perSeconds": 60 },
  "guard": { "id": "<guard_id>" },
  "users": [{ "id": "<user_id_1>" }, { "id": "<user_id_2>" }]
}

Rejection Response

When a guard blocks a request, the response is structured so clients can programmatically determine why they were blocked and when to retry.

Error Codes

Reason errorCode HTTP Status
Rate limit exceeded RATE_LIMIT_EXCEEDED 429
IP not in whitelist IP_NOT_ALLOWED 403
IP in blacklist IP_BLOCKED 403

REST Response Body

{
  "statusCode": 429,
  "message": "Too Many Requests",
  "errorCode": "RATE_LIMIT_EXCEEDED",
  "details": {
    "reason": "rate_limit",
    "scope": "ip",
    "limit": 100,
    "remaining": 0,
    "windowSeconds": 60,
    "retryAfterSeconds": 42,
    "resetAt": 1722650400000
  }
}

For IP rejections, details is minimal:

{
  "statusCode": 403,
  "message": "Forbidden",
  "errorCode": "IP_BLOCKED",
  "details": { "reason": "ip_blocked" }
}

Response Headers

On rejection (429):

Header Description
Retry-After Seconds until the client can retry
X-RateLimit-Limit Max requests in the window
X-RateLimit-Remaining Requests remaining (0 when blocked)
X-RateLimit-Reset Epoch ms when the window resets
X-RateLimit-Window Window duration in seconds
X-RateLimit-Scope ip, user, or route
X-RateLimit-Used Requests consumed in this window
X-Enfyra-Guard-Reason rate_limit, ip_not_allowed, or ip_blocked
X-Enfyra-Guard-Error-Code Same as body errorCode
X-Enfyra-Guard-Scope Rate-limit scope (rate limit only)

On successful guarded requests:

Even when the request passes, Enfyra exposes the strictest evaluated rate-limit bucket (lowest remaining / limit ratio) so clients can proactively throttle:

Header Description
X-RateLimit-Limit Max requests in the window
X-RateLimit-Remaining Requests remaining
X-RateLimit-Reset Epoch ms when the window resets
X-RateLimit-Window Window duration in seconds
X-RateLimit-Scope ip, user, or route
X-RateLimit-Used Requests consumed

GraphQL Rejection

Guard rejections in GraphQL resolvers return structured extensions:

{
  "errors": [{
    "message": "Too Many Requests",
    "extensions": {
      "code": "RATE_LIMIT_EXCEEDED",
      "statusCode": 429,
      "details": {
        "reason": "rate_limit",
        "scope": "ip",
        "limit": 100,
        "remaining": 0,
        "windowSeconds": 60,
        "retryAfterSeconds": 42,
        "resetAt": 1722650400000
      }
    }
  }]
}

What Is Never Exposed

Guard internals stay server-side: guard names, rule IDs, Redis keys, configured IP lists, and raw user IDs are never included in client-facing responses.

Guard Alerts

Every guard rejection is automatically recorded in the enfyra_guard_alert system table for admin monitoring.

Column Description
scope ip, user, or route
scopeKey The subject that was blocked (IP address, user ID, or route path)
routePath The route that rejected the request
method HTTP method of the rejected request
errorCode RATE_LIMIT_EXCEEDED, IP_NOT_ALLOWED, or IP_BLOCKED
guardName Name of the guard that rejected (for admin reference)
createdAt When the rejection occurred

Alerts are visible in Settings > Admin > Runtime Monitor > Guards tab, which shows repeated offenders grouped by subject and a chronological rejection log.

The alert route (/enfyra_guard_alert) supports GET and DELETE only — records are created automatically by the guard middleware and cannot be inserted via API.

Guards vs. preHook Rate Limiting

Aspect Guards preHook ($helpers.$rateLimit)
Setup Admin UI / API (no code) Custom script
Runs before auth? Yes (pre_auth) No
Tree logic (AND/OR) Built-in Manual scripting
Best for Standard IP/rate protection Custom dynamic logic

Use guards for standard, config-driven protection. Use preHook rate limiting when you need custom logic (conditional limits, dynamic keys, etc.).

Notes

  • rate_limit_by_user is only available in post_auth position (needs user context)
  • Rules within a guard are evaluated cheapest first (IP checks before rate limit counters)
  • Guards apply per instance — changes sync across instances via Redis Pub/Sub
  • priority controls execution order — lower values run first

Next Steps