PRD 23 — Cloudflare Strategy
Last updated: 2026-04-13 Status: Active — reflects implementation state as of 2026-04-13
1. Purpose & Scope
Section titled “1. Purpose & Scope”This document consolidates the complete Cloudflare strategy for the Savoy platform:
- Edge caching architecture and TTLs
- Cache invalidation (CMS-native purge system)
- Rate limiting (plan-dependent)
- Plan strategy per environment
- Security perimeter (WAF, rate limits, bot protection)
- Cost matrix
For implementation details, see:
docs/dev-backend-guides/04_WEBHOOKS_AND_CACHE_PURGE.md— CMS-side cache purge (CachePurgeService, scenarios S1-S7)docs/dev-backend-guides/10_CLOUDFLARE_CACHE_RULES.md— Cache rules + rate limit rules + setup scriptsdocs/PRD/02_Infrastructure_and_Environments.md§ 3.4 — Plan matrix contextdocs/PRD/09_Cache_and_Performance.md— Multi-layer cache architecturedocs/PRD/15_Security_and_Data_Protection.md§ 2 — Security perimeter details
2. Edge Cache Architecture
Section titled “2. Edge Cache Architecture”2.1 Cache Layers
Section titled “2.1 Cache Layers”| Layer | Technology | TTL Strategy | Invalidation |
|---|---|---|---|
| Browser | Cache-Control: max-age=0 | Revalidates every request | N/A (server controls via s-maxage) |
| Cloudflare Edge | public, s-maxage=31536000, swr=60 | 1 year max, serves stale during revalidate | Explicit purge via CachePurgeService |
Next.js use cache | In-memory (short-lived) | 5-15 min for nav tree, dictionary | Process restart |
| Umbraco Content Cache | In-memory | Invalidated on publish | Built-in |
| SQL Content | Umbraco source of truth | — | — |
2.2 Route-Specific Cache-Control Headers
Section titled “2.2 Route-Specific Cache-Control Headers”(Match what’s in apps/web/next.config.ts headers())
| Route | Cache-Control | CDN-Cache-Control | Edge behavior |
|---|---|---|---|
/ + localized routes (pt/en/es/fr/de/…) via negative lookahead | public, max-age=0, s-maxage=31536000, stale-while-revalidate=60 | public, max-age=31536000 | Cached 1 year, purged on content publish |
/_next/static/* | public, max-age=31536000, immutable | same | Cached forever (hashed filenames) |
/_next/image | public, max-age=2592000, stale-while-revalidate=86400 | public, max-age=2592000 | 30 days |
/media/* (Umbraco proxy) | public, max-age=2592000, stale-while-revalidate=86400 | public, max-age=31536000 | 30 days browser, 1 year edge |
/api/* | private, no-store, no-cache, must-revalidate | no-store | BYPASS |
/gate/* | private, no-store, no-cache, must-revalidate | no-store | BYPASS |
Locale matcher is /:path((?!api/|gate/|_next/|media/|favicon\.ico|robots\.txt|sitemap).*) — locale-agnostic so new languages work automatically.
3. Cloudflare Plan Strategy
Section titled “3. Cloudflare Plan Strategy”3.1 Plan per Environment
Section titled “3.1 Plan per Environment”| Environment | Zone | Plan | Monthly cost | Status |
|---|---|---|---|---|
| DEV | wycreative.com (shared) | Free | €0 (shared) | Active |
| STAGE | wycreative.com (shared) | Free | €0 (shared) | Pending |
| QA | savoysignature.com | Pro | ~€22 | Pending provisioning |
| PROD | savoysignature.com + hotelnext.pt | Pro | ~€44 (2 zones) | Pending provisioning |
3.2 Plan Capability Matrix
Section titled “3.2 Plan Capability Matrix”| Feature | Free | Pro | Savoy usage |
|---|---|---|---|
| Cache Rules | Limited | Full | 6 rules (DEV already configured) |
| Rate Limit Rules | 1 rule, 10s period only | 10 rules, 10/60/600/3600s periods | 1 combined (DEV), 3 granular (QA/PROD) |
| Mitigation timeout | 10s minimum | Up to 1h | Gate login brute-force needs 5min (Pro-only) |
| Managed Ruleset | Strict (limited) | Strict (full) | Active on all envs |
| Bot Management | Basic | Advanced | Active |
| Transform Rules | 10 | 50 | — |
| Page Rules | 3 | 25 | — |
| Image Optimization (Polish, WebP, Mirage) | ❌ | ✓ | Planned for PROD |
| Tiered Caching | ❌ | ✓ | Planned for PROD |
3.3 Pro Plan Provisioning Checklist (before QA go-live)
Section titled “3.3 Pro Plan Provisioning Checklist (before QA go-live)”- Client approves monthly cost (~€44/mo for QA + PROD zones)
- Upgrade wycreative.com zone OR move QA/PROD to savoysignature.com/hotelnext.pt zones (confirm DNS strategy)
- Run
CF_PLAN=pro bash scripts/cloudflare/setup-rate-limits.shto deploy 3 granular rules - Run
bash scripts/cloudflare/setup-cache-rules.sh(if cache rules differ per plan) - Enable Image Optimization (Polish, Mirage) in Speed settings
- Enable Tiered Caching (Argo)
- Verify WAF Managed Ruleset (Strict) active
- Update
docs/PRD/11§ 3.1 status to “Active”
4. Cache Rules (6 rules, API-managed)
Section titled “4. Cache Rules (6 rules, API-managed)”See full rule definitions in docs/dev-backend-guides/10_CLOUDFLARE_CACHE_RULES.md § Cache Rules. Summary:
| # | Match | Action | Notes |
|---|---|---|---|
| 1 | /api/* on Savoy hosts | Bypass cache | Dynamic responses |
| 2 | /gate/* on Savoy hosts | Bypass cache | Auth-sensitive |
| 3 | /umbraco* or CMS hosts | Bypass cache | Backoffice |
| 4 | /_next/static/* | Cache 1 year override | Hashed filenames, safe forever |
| 5 | /_next/image* or /media/* | Cache 30 days override | Purged on content change |
| 6 | All Savoy frontend hosts (catch-all) | Respect origin s-maxage | HTML pages cached 1 year via headers |
Script: scripts/cloudflare/setup-cache-rules.sh (idempotent, PUT to /rulesets/phases/http_request_cache_settings/entrypoint)
Token permission: Zone.Cache Rules:Edit
5. Rate Limiting Rules (plan-dependent)
Section titled “5. Rate Limiting Rules (plan-dependent)”5.1 Free plan (DEV/STAGE) — 1 combined rule
Section titled “5.1 Free plan (DEV/STAGE) — 1 combined rule”| Match | Limit | Mitigation |
|---|---|---|
/api/forms/* POST OR /api/gate/login POST on Savoy hosts | 2 req / 10s per IP | 10s block |
Free plan limitations:
- Only 1 rule total
- Period fixed at 10 seconds
- Mitigation timeout minimum 10 seconds
5.2 Pro plan (QA/PROD) — 3 granular rules
Section titled “5.2 Pro plan (QA/PROD) — 3 granular rules”| # | Match | Limit | Mitigation |
|---|---|---|---|
| 1 | /api/forms/* POST | 5 req / 60s per IP | 60s |
| 2 | /api/search | 20 req / 60s per IP | 60s |
| 3 | /api/gate/login POST | 10 req / 60s per IP | 300s (anti-brute-force) |
Script: scripts/cloudflare/setup-rate-limits.sh with CF_PLAN=free|pro
Token permission: Zone.Zone WAF:Edit
5.3 Why edge rate limiting (not in-app)
Section titled “5.3 Why edge rate limiting (not in-app)”- Stateless Next.js: In-memory counters break across multi-instance deployments (PROD runs 2-6 instances). See
.claude/rules/anti-patterns.md#91. - Origin protection: Blocks abusive traffic before it consumes Azure CPU/bandwidth.
- Horizontal scaling: No coordination needed between instances.
- IP extraction in the app: Still done via CF-Connecting-IP / X-Forwarded-For headers, but ONLY for audit logging, never enforcement.
6. CMS-Native Cache Purge System
Section titled “6. CMS-Native Cache Purge System”Purge is triggered entirely from Umbraco via notification handlers. No Next.js webhook involved.
6.1 Architecture
Section titled “6.1 Architecture”See docs/dev-backend-guides/04_WEBHOOKS_AND_CACHE_PURGE.md for full details. Components:
CachePurgeComposer— DI registration + notification handlers (ContentPublished, ContentUnpublished, MediaSaved)CachePurgeService— Fire-and-forget orchestration (withExecutionContext.SuppressFlowto isolate scope from notification thread)ContentDependencyResolver— Classifies content, resolves URL paths (withIShortStringHelper.CleanStringForUrlSegmentfor diacritic handling), walks content tree, searches JSON for GUIDsCloudflareClient— API calls (batched 30 URLs per request), warmup (GET fire-and-forget, cap 30),PurgeAllAsyncfor zone-wide purgePurgeLogRepository— NPoco-based SQL reads/writes toSavoyPurgeLogtable (Timestampmust bedatetimeoffset(7), notdatetime)PurgeLogCleanupService— BackgroundService deleting entries >30 days old
6.2 Purge Scenarios
Section titled “6.2 Purge Scenarios”| # | Trigger | Scope |
|---|---|---|
| S1 | Publish regularPage (contentPage) | Page URL (PT+EN) on affected site |
| S2 | Publish detailPage (room/dining/wellness/experience/event/offer) | Page + parent list page |
| S3 | Publish listPage | List page URL |
| S4 | Publish siteRoot | ALL descendant pages on that site (+ /404 /500) |
| S5 | Publish repositoryItem (eventItem, faqItem, etc.) | JSON GUID search → pages referencing the item GUID or parent repo GUID |
| S6 | Save media | JSON GUID search → pages referencing the media GUID + direct media URLs across all sites |
| S7 | Unpublish any page | Same as S1/S2 (by content type) |
| S8 | Error pages | Included in S4 as /404 and /500 |
6.3 Manual Purge (admin-only dashboard)
Section titled “6.3 Manual Purge (admin-only dashboard)”Dashboard “Cache & Purge” tab has:
- Purge All button (with confirmation modal) → calls
POST /umbraco/api/savoy-dashboard/purge-all - Purge by URL input → calls
POST /umbraco/api/savoy-dashboard/purge-urls(max 100) - Recent Purge Events list (from
SavoyPurgeLog) - KPI cards: Cache Hit Rate, Bandwidth Saved, Purges (24h), URLs Purged
Dashboard “Cache Purge” tab (renamed from “Webhook Config”) shows:
- CachePurge enabled/disabled
- Cloudflare Zone ID configured
- API Token configured
- PurgeLog table available
- Subscribed notification events
- Supported purge scenarios (S1-S7)
- Last purge event
Debug endpoint: GET /umbraco/api/savoy-dashboard/debug/purge-log (direct SQL read, no cache)
7. Performance Targets & Monitoring
Section titled “7. Performance Targets & Monitoring”- TTFB (cached HTML): < 50ms (verified on DEV: ~45-53ms)
- TTFB (cached vs origin): 55% improvement (53ms vs 118ms)
- Cache hit ratio: > 80% (monitored via Cloudflare Observatory + Dashboard Performance tab)
- Purge latency: < 2s from publish to Cloudflare API completion
- Warmup coverage: 30 URLs max per event (fire-and-forget)
Monitoring:
- Umbraco Dashboard → Cache & Purge tab (PurgeLog SQL, Cloudflare GraphQL)
- Umbraco Dashboard → Performance tab (Cloudflare Observatory, Web Vitals)
- Azure Application Insights (origin latency, errors)
8. Acceptance Criteria
Section titled “8. Acceptance Criteria”- CMS-native cache purge operational (no Next.js webhook)
- 8 purge scenarios covered by ContentDependencyResolver
- PurgeLog writes and reads working (DateTimeOffset type fixed)
- Dashboard tabs functional (Cache & Purge + Cache Purge config)
- Cache Rules deployed (6 rules via API on wycreative.com)
- HTML cached at edge (verified
cf-cache-status: HIT) - TTFB < 50ms on cached pages
- ARR Affinity disabled (prerequisite for edge caching)
- In-memory rate limiter removed from Next.js
- Rate limiting at Cloudflare WAF layer
- Pro plan provisioned for QA (before QA go-live)
- Pro plan provisioned for PROD (before PROD go-live)
- 3 granular rate limit rules (deployed via CF_PLAN=pro)
- Image optimization enabled on Pro (Polish, Mirage, WebP auto)
9. Outstanding Items
Section titled “9. Outstanding Items”- Cloudflare Pro upgrade — Tracked in
docs/todo/todo-infra.md§ 2.1. Required before QA go-live. - Rate limit tuning — Current DEV rule (2 req/10s) is strict; monitor false positives in user testing.
- Zone strategy — Decide if production uses wycreative.com (shared) or savoysignature.com + hotelnext.pt (dedicated). Impacts rate limit scope and cost.