Handoff: Cloudflare API Security — WAF, Rate Limiting, Bot Protection & Caching
Implementation plan for adding Cloudflare security layers to all 4 backend APIs
Handoff: Cloudflare API Security for All Backend APIs
From: Session 99 analysis (Cursor Agent) To: Future session agent Priority: High — APIs currently have no WAF, rate limiting, or bot protection Estimated Scope: ~2-3 hours Cloudflare dashboard + ~2 hours code changes Requires: Cloudflare dashboard access (dash.cloudflare.com)
Context & Motivation
What We Analyzed (Session 99)
A full audit of the Cloudflare integration revealed that:
- Cloudflare Pages service (
apis/backend-api/src/app/cloudflare/cloudflare-pages.service.ts) is exclusively a frontend static deployment utility. It has zero API proxy/security functionality. - DNS is managed in Cloudflare with proxied (orange cloud) CNAME records pointing API subdomains to Railway.
- No Cloudflare security features are configured for API traffic — no WAF, no rate limiting, no bot detection.
- CORS is the only access control — and CORS is a browser-enforced policy, not a security boundary. Any
curlor script bypasses it entirely.
Current Architecture (As-Is)
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLOUDFLARE (DNS Proxy) │
│ │
│ What it DOES: │
│ ✅ SSL termination │
│ ✅ Basic L3/L4 DDoS absorption │
│ ✅ IP masking of Railway origins │
│ │
│ What it DOES NOT do (gap): │
│ ❌ WAF (Web Application Firewall) │
│ ❌ Rate limiting │
│ ❌ Bot detection │
│ ❌ API response caching │
│ ❌ Managed security rulesets │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RAILWAY │
│ │
│ api.gograb.space → backend-api (port 3000) │
│ ai-api.gograb.space → backend-ai-api (port 3001) │
│ docs-api.gograb.space → backend-docs-api (port 3002) │
│ vitto-api.gograb.space → backend-vitto-api (port 3003) │
│ │
└─────────────────────────────────────────────────────────────────┘
Target Architecture (To-Be)
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLOUDFLARE (Security Gateway) │
│ │
│ ✅ SSL termination │
│ ✅ L3/L4 DDoS absorption │
│ ✅ IP masking │
│ ✅ WAF managed rulesets (OWASP Top 10) ← NEW │
│ ✅ Rate limiting (tiered by endpoint) ← NEW │
│ ✅ Bot Fight Mode ← NEW │
│ ✅ Cache-Control response caching (selective) ← NEW │
│ │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RAILWAY │
│ (same services, now protected by Cloudflare security layer) │
└─────────────────────────────────────────────────────────────────┘
What Was Explicitly Rejected (and Why)
| Option | Verdict | Reasoning |
|---|---|---|
| Cloudflare Workers API Gateway | Rejected | NestJS monoliths with MongoDB can't decompose to edge functions. Workers have 10ms CPU limit (free) / 50ms (paid). Would create two runtimes to maintain and a debugging nightmare. |
| Cloudflare Tunnel | Deferred | Requires cloudflared daemon in Dockerfiles. Complicates branch-based Railway deployment. Marginal security gain at current scale. Revisit if handling sensitive data or under targeted attacks. |
| Cloudflare API Shield | Deferred | Enterprise-tier cost. Not justified until third-party API consumers emerge. |
Scope: All 4 Backend APIs
This handoff applies to all backend services simultaneously:
| Service | Subdomain | Railway Domain |
|---|---|---|
| backend-api | api.gograb.space |
backend-api-production-97c0.up.railway.app |
| backend-ai-api | ai-api.gograb.space |
backend-ai-api-production.up.railway.app |
| backend-docs-api | docs-api.gograb.space |
backend-docs-api-production.up.railway.app |
| backend-vitto-api | vitto-api.gograb.space |
TBD (pending Railway deployment) |
Implementation Plan
Phase 1: Enable Bot Fight Mode (5 minutes)
Where: Cloudflare Dashboard > Security > Bots
Steps:
- Navigate to
dash.cloudflare.com>gograb.space> Security > Bots - Enable Bot Fight Mode (free tier toggle)
- This immediately starts challenging automated/bot traffic
Verification:
- Check Security > Overview for bot detection events after 24h
- No code changes required
Risk: Minimal. Bot Fight Mode uses JavaScript challenges that legitimate browsers handle transparently. API clients sending proper headers are unaffected.
Phase 2: Enable WAF Managed Rules (30 minutes)
Where: Cloudflare Dashboard > Security > WAF
Steps:
- Navigate to Security > WAF > Managed rules
- Enable Cloudflare Managed Ruleset — blocks common attacks (SQLi, XSS, etc.)
- Enable Cloudflare OWASP Core Ruleset — broader OWASP Top 10 coverage
- Set action to Block for high-confidence rules, Log for medium-confidence initially
Important: Scoping to API subdomains only
Create a WAF custom rule that targets API traffic:
Expression: (http.host contains "api.gograb.space") or
(http.host eq "ai-api.gograb.space") or
(http.host eq "docs-api.gograb.space") or
(http.host eq "vitto-api.gograb.space")
This ensures WAF rules only apply to API subdomains, not frontend Pages projects.
Free Tier Limitation: Free plan allows 5 custom WAF rules. If more are needed, Pro plan ($20/mo) provides managed rulesets.
Verification:
- Test with a known-safe SQLi payload:
curl "https://api.gograb.space/api/test?q=1'OR'1'='1"— should be blocked - Check Security > Overview for WAF events
- Monitor for false positives over 48 hours before switching medium-confidence rules from Log to Block
Risk: Low. Start with Log mode for medium-confidence rules. Block only high-confidence threats initially.
Phase 3: Configure Rate Limiting Rules (1 hour)
Where: Cloudflare Dashboard > Security > WAF > Rate limiting rules
Rule 1: Auth Endpoints (Strict)
Name: Auth Rate Limit
Expression: (http.request.uri.path contains "/auth/" or
http.request.uri.path contains "/login" or
http.request.uri.path contains "/register" or
http.request.uri.path contains "/password")
and
(http.host contains "api.gograb.space" or
http.host eq "ai-api.gograb.space" or
http.host eq "vitto-api.gograb.space")
Rate: 10 requests per 1 minute
Counting: Per IP
Action: Block for 10 minutes
Response: 429 Too Many Requests
Rule 2: General API (Moderate)
Name: General API Rate Limit
Expression: (http.host contains "api.gograb.space") or
(http.host eq "ai-api.gograb.space") or
(http.host eq "docs-api.gograb.space") or
(http.host eq "vitto-api.gograb.space")
Rate: 100 requests per 1 minute
Counting: Per IP
Action: Block for 5 minutes
Response: 429 Too Many Requests
Rule 3: Health Check Exclusion
Name: Health Check Bypass
Expression: http.request.uri.path contains "/health"
Action: Skip (no rate limiting)
IMPORTANT: Rule 3 must be ordered BEFORE Rules 1 and 2, so health checks from Railway monitoring and uptime services are never rate-limited.
Verification:
- Use
aborwrkto send 15 rapid requests to an auth endpoint — should get 429 after 10 - Confirm health check endpoint remains accessible under load
- Monitor Cloudflare analytics for rate limiting events
Risk: Moderate — rate limits need tuning based on actual traffic patterns. Start with these values and adjust after 1 week of monitoring. If legitimate users hit limits, increase thresholds.
Adjustment guidance:
- If frontend app makes burst requests on page load, count those and set the general limit above that burst
- If backend-to-backend communication happens via public URLs, whitelist those IPs or use higher limits
Phase 4: Add Cache-Control Headers for Public Endpoints (2 hours — code changes)
Where: NestJS backend code
This is the only phase requiring code changes. Add Cache-Control headers to specific public endpoints so Cloudflare caches responses at the edge.
4a. Create a shared caching interceptor
File: libs/libs-api/backend-core/src/lib/interceptors/cache-control.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, SetMetadata } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { Reflector } from '@nestjs/core';
export const CACHE_TTL_KEY = 'cache-control-ttl';
export const CacheTTL = (seconds: number) => SetMetadata(CACHE_TTL_KEY, seconds);
@Injectable()
export class CacheControlInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const ttl = this.reflector.get<number>(CACHE_TTL_KEY, context.getHandler());
return next.handle().pipe(
tap(() => {
if (ttl && ttl > 0) {
const response = context.switchToHttp().getResponse();
response.setHeader('Cache-Control', `public, max-age=${ttl}, s-maxage=${ttl}`);
}
}),
);
}
}
4b. Apply to specific endpoints
Health endpoints — all backends:
@Get('health')
@CacheTTL(30) // Cache for 30 seconds
getHealth() { ... }
Public docs content — backend-docs-api:
@Get('public/:slug')
@CacheTTL(300) // Cache for 5 minutes
getPublicDoc() { ... }
Static config/metadata — backend-api:
@Get('config/public')
@CacheTTL(3600) // Cache for 1 hour
getPublicConfig() { ... }
4c. Cloudflare cache rule (dashboard)
Create a Cache Rule in Cloudflare:
Name: Respect API Cache-Control Headers
Expression: (http.host contains "api.gograb.space") or
(http.host eq "ai-api.gograb.space") or
(http.host eq "docs-api.gograb.space") or
(http.host eq "vitto-api.gograb.space")
Cache eligibility: Eligible for cache
Respect origin Cache-Control headers: Yes
By default, Cloudflare does NOT cache API responses (non-static content types). This rule tells it to respect the Cache-Control headers we set in code.
Verification:
curl -I https://api.gograb.space/api/health— check forCF-Cache-Status: HITon second requestcurl -I https://api.gograb.space/api/auth/login— should showCF-Cache-Status: DYNAMIC(not cached)
Risk: Low if implemented correctly. Only endpoints explicitly decorated with @CacheTTL() will be cached. Authenticated endpoints remain dynamic.
4d. Export from backend-core barrel
File: libs/libs-api/backend-core/src/index.ts
Add export:
export { CacheControlInterceptor, CacheTTL, CACHE_TTL_KEY } from './lib/interceptors/cache-control.interceptor';
WebSocket Considerations (Vitto API)
The Vitto API uses WebSockets via mcp-gateway.gateway.ts. Cloudflare proxy mode (orange cloud) supports WebSockets, but:
| Plan | Concurrent WS Connections | Notes |
|---|---|---|
| Free | ~100 per domain | Soft limit, may vary |
| Pro ($20/mo) | Higher (undisclosed) | Recommended if scaling |
| Enterprise | Unlimited | Not needed now |
Action items:
- After Vitto goes live behind Cloudflare proxy, monitor concurrent WebSocket connections
- If connections are refused or dropped, check Cloudflare analytics for WS errors
- If limits hit, either upgrade to Pro or switch Vitto subdomain to DNS-only (grey cloud) as a quick fix — losing WAF/rate limiting for that subdomain only
Files That Will Be Modified
| # | File | Action | Phase |
|---|---|---|---|
| 1 | libs/libs-api/backend-core/src/lib/interceptors/cache-control.interceptor.ts |
Create | Phase 4 |
| 2 | libs/libs-api/backend-core/src/index.ts |
Modify — add export | Phase 4 |
| 3 | apis/backend-api/src/app/**/*.controller.ts |
Modify — add @CacheTTL decorators | Phase 4 |
| 4 | apis/backend-ai-api/src/app/**/*.controller.ts |
Modify — add @CacheTTL decorators | Phase 4 |
| 5 | apis/backend-docs-api/src/app/**/*.controller.ts |
Modify — add @CacheTTL decorators | Phase 4 |
| 6 | apis/backend-vitto-api/src/app/**/*.controller.ts |
Modify — add @CacheTTL decorators | Phase 4 |
Phases 1-3 are dashboard-only — zero code changes.
Verification Checklist (Post-Implementation)
Security Verification
- Bot Fight Mode enabled (Security > Bots)
- WAF managed rules active (Security > WAF)
- Rate limiting rules configured (3 rules)
- Health check exclusion rule ordered first
Endpoint Testing
-
curl https://api.gograb.space/api/health— 200 OK, not rate-limited -
curl https://ai-api.gograb.space/ai-api/health— 200 OK, not rate-limited -
curl https://docs-api.gograb.space/docs-api/health— 200 OK, not rate-limited -
curl https://vitto-api.gograb.space/vitto-api/health— 200 OK, not rate-limited
Security Testing
- SQLi attempt blocked:
curl "https://api.gograb.space/api/test?q=1'OR'1'='1"— 403 - Rate limit triggered: 15 rapid auth requests → 429 after 10th
- Normal usage unaffected: frontend app works normally
Caching Testing (Phase 4 only)
- Health endpoint cached: second request shows
CF-Cache-Status: HIT - Auth endpoint NOT cached:
CF-Cache-Status: DYNAMIC - Cache TTLs correct per decorator values
Monitoring (24-48 hours post-deployment)
- No false positives in WAF logs
- No legitimate users rate-limited
- WebSocket connections to Vitto stable
- Frontend → API calls unaffected
Rollback Plan
All changes are instantly reversible:
| Issue | Rollback |
|---|---|
| WAF blocking legitimate traffic | Disable specific WAF rule or switch to Log mode |
| Rate limit too aggressive | Increase threshold or disable rule |
| Bot Fight Mode causing issues | Toggle off (Security > Bots) |
| Caching stale data | Remove @CacheTTL decorator or set to 0 |
| WebSocket issues on Vitto | Switch vitto-api.gograb.space to DNS-only (grey cloud) |
Cost Analysis
| Item | Current | After Implementation |
|---|---|---|
| Cloudflare plan | Free | Free (or Pro $20/mo for managed WAF rulesets) |
| Railway costs | Unchanged | Slightly reduced (cached responses reduce Railway load) |
| Code maintenance | None | Minimal (CacheControlInterceptor is ~20 lines) |
Recommendation: Start on Free tier. If WAF managed rulesets are desired beyond the 5 custom rules, upgrade to Pro ($20/mo) — still very cost-effective for the security it provides.
Decision Log References
These decisions are logged in MCP and can be retrieved with:
find_similar({ query: "Cloudflare API security WAF rate limiting" })
| Decision ID | Topic |
|---|---|
dev_1770496457666_4mnfw5y |
Overall Cloudflare API security strategy |
dev_1770496462813_ndb2agg |
Rate limiting tier design |
dev_1770496469974_5f03l9l |
Rejected options (Workers, Tunnel, API Shield) |
dev_1770496475908_jdt2ubq |
WebSocket considerations for Vitto |
Created: Session 99 — February 7, 2026 Author: Cursor Agent (architecture analysis) Status: Planned — awaiting dedicated implementation session