This implementation adds a configurable, Redis-backed rate-limiting layer to protect the StellarFlow Oracle from DDoS attacks and excessive API consumption. The system supports:
- Distributed throttling via Redis (with graceful fallback to in-memory store)
- Real-time configuration via Admin Dashboard API
- IP whitelisting for relayers and admin IPs
- Hot-reload from
config.jsonwithout server restart
-
Rate Limit Middleware (
src/middleware/rateLimitMiddleware.ts)- Uses
express-rate-limitwithrate-limit-redisstore - Reads config from
appConfig.rateLimiton every request (dynamic) - Maintains in-memory cache of whitelisted IPs (refreshed every 60s)
- Resolves real client IP from
X-Forwarded-ForwhenTRUST_PROXY=true
- Uses
-
Config Management (
src/config/configWatcher.ts)- Extended
AppConfiginterface withRateLimitConfig - Hot-reloads
config.jsonchanges viafs.watch - Defaults: 100 requests per 15 minutes, enabled
- Extended
-
Database Schema (
prisma/schema.prisma)- Added
whitelistedIps: String[]toRelayermodel - Allows per-relayer IP whitelist configuration
- Added
-
Admin API (
src/routes/admin.ts)GET /api/admin/rate-limit- View current configPUT /api/admin/rate-limit- Update config in real-timePOST /api/admin/rate-limit/whitelist/refresh- Force-refresh IP cache
Add to .env:
# Set to "true" if behind a reverse proxy (nginx, AWS ALB, etc.)
# Enables X-Forwarded-For header parsing for real client IP
TRUST_PROXY=false
# Redis URL (required for distributed rate limiting across multiple instances)
REDIS_URL=redis://localhost:6379
# Admin IP (automatically whitelisted from rate limits)
ADMIN_IP=127.0.0.1
ADMIN_API_KEY=your_admin_key_here{
"rateLimit": {
"windowMs": 900000, // 15 minutes in milliseconds
"maxRequests": 100, // Max requests per IP per window
"enabled": true // Global throttling toggle
}
}curl -X GET http://localhost:3000/api/admin/rate-limit \
-H "x-admin-key: your_admin_key" \
-H "x-api-key: your_api_key"Response:
{
"success": true,
"rateLimit": {
"windowMs": 900000,
"maxRequests": 100,
"enabled": true
}
}curl -X PUT http://localhost:3000/api/admin/rate-limit \
-H "x-admin-key: your_admin_key" \
-H "x-api-key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"maxRequests": 200,
"windowMs": 600000,
"enabled": true
}'Response:
{
"success": true,
"message": "Rate-limit configuration updated",
"rateLimit": {
"windowMs": 600000,
"maxRequests": 200,
"enabled": true
}
}Note: Changes take effect immediately on the next request. The config is also persisted to config.json so it survives server restarts.
curl -X PUT http://localhost:3000/api/admin/rate-limit \
-H "x-admin-key: your_admin_key" \
-H "x-api-key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'Use Prisma Studio or direct SQL:
UPDATE "Relayer"
SET "whitelistedIps" = ARRAY['203.0.113.10', '203.0.113.11']
WHERE name = 'primary-relayer';Or via Prisma:
await prisma.relayer.update({
where: { id: 1 },
data: {
whitelistedIps: ["203.0.113.10", "203.0.113.11"],
},
});After updating relayer IPs in the database:
curl -X POST http://localhost:3000/api/admin/rate-limit/whitelist/refresh \
-H "x-admin-key: your_admin_key" \
-H "x-api-key: your_api_key"Response:
{
"success": true,
"message": "IP whitelist cache refreshed"
}Note: The whitelist cache auto-refreshes every 60 seconds, so manual refresh is optional.
Run the Prisma migration to add whitelistedIps to the Relayer table:
npx prisma migrate dev --name add_relayer_ip_whitelistOr for production:
npx prisma migrate deployThe rate limiter requires Redis for distributed throttling across multiple server instances. If Redis is unavailable, the middleware gracefully falls back to an in-memory store (not shared across instances).
Docker Compose:
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
redis-data:Environment:
REDIS_URL=redis://localhost:6379If the app is behind a reverse proxy (nginx, AWS ALB, Cloudflare), set TRUST_PROXY=true so the middleware reads the real client IP from X-Forwarded-For.
Nginx example:
location /api {
proxy_pass http://localhost:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}- Without reverse proxy: Set
TRUST_PROXY=false(default). The middleware usesreq.ipdirectly. - With reverse proxy: Set
TRUST_PROXY=trueand ensure the proxy is configured to setX-Forwarded-Forcorrectly. Only trust proxies you control.
- Admin IP (
ADMIN_IPenv var) is automatically whitelisted - Relayer IPs (
Relayer.whitelistedIps) are whitelisted whenisActive=true - IPv4-mapped IPv6 addresses (
::ffff:1.2.3.4) are normalized to plain IPv4 for comparison
The middleware returns standard rate-limit headers:
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1714089600
When rate limit is exceeded:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"success": false,
"error": "Too many requests. Limit: 100 per 15 minutes.",
"retryAfter": 900
}
Check Redis connection status:
redis-cli ping
# Expected: PONGView rate-limit keys:
redis-cli --scan --pattern "rl:*"The middleware logs:
[RateLimit] Redis unavailable — using in-memory store(warning)[RateLimit] Failed to refresh IP whitelist cache(error)[AdminRateLimit] Rate-limit config updated: {...}(info)
Rate-limit hits are tracked per IP in Redis with automatic expiry. Use Redis monitoring tools or the /api/v1/cache endpoint to observe cache metrics.
# Send 101 requests rapidly (should trigger 429 on the 101st)
for i in {1..101}; do
curl -s -o /dev/null -w "%{http_code}\n" \
-H "x-api-key: your_api_key" \
http://localhost:3000/api/v1/market-rates/rates
done- Add your IP to a relayer's
whitelistedIps - Send 200 requests (should all succeed, no 429)
- Remove your IP and retry (should get 429 after 100 requests)
- Set
maxRequests: 5via admin API - Send 6 requests (should get 429 on the 6th)
- Set
enabled: falsevia admin API - Send 100 requests (should all succeed)
- Check Redis connection:
redis-cli ping - Verify
REDIS_URLis set correctly - Check logs for
[RateLimit] Redis unavailablewarning - Ensure
rateLimit.enabled: trueinconfig.json
- Verify IP is in
Relayer.whitelistedIpsor matchesADMIN_IP - Check IP normalization (IPv4 vs IPv6-mapped)
- Force-refresh whitelist cache via admin API
- Check logs for
[RateLimit] Failed to refresh IP whitelist cache
- Verify
config.jsonsyntax is valid JSON - Check file permissions (must be readable by the app)
- Restart the server if
configWatcheris not running - Use admin API to update config instead of editing
config.jsondirectly
- Per-endpoint rate limits (e.g., stricter limits on
/price-updates) - Per-relayer rate limits (in addition to global limits)
- Rate-limit metrics dashboard (Prometheus/Grafana)
- Automatic IP ban after repeated 429s
- CAPTCHA challenge for suspicious IPs
- Geo-blocking for high-risk regions