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_useris only available inpost_authposition (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
prioritycontrols execution order — lower values run first
Next Steps
- See API Lifecycle for the full request flow
- See Hooks and Handlers for code-driven protection
- See Context Reference - Rate Limiting for preHook rate limiting