This document describes the overall system architecture for the Savoy Signature multi-site headless platform. It defines the layers, component boundaries, data flow, and key architectural decisions that govern how the frontend, CMS, CDN, and external services interact.
Cloudflare sits in front of all public traffic. Its responsibilities:
Feature
Description
DNS
All 8 site domains point to Cloudflare, which proxies to Azure
SSL
Full (Strict) mode, end-to-end HTTPS
HTML Cache
Aggressive edge caching of SSR output (Cache-Control: public, max-age=0, s-maxage=31536000, stale-while-revalidate=60). max-age=0 prevents stale browser cache; s-maxage controls Cloudflare edge TTL; stale-while-revalidate serves stale during refresh. “Purge All” option available in Umbraco custom dashboard
Cache Purge
Programmatic purge via Cloudflare API, triggered by Umbraco publish webhooks
WAF
Web Application Firewall rules for bot protection and rate limiting
Page Rules
Per-domain rules for cache behavior, redirect rules
Key principle: Pages are cached indefinitely at the edge. Content changes trigger targeted purge + warmup. This eliminates most origin requests.
Cache SSR output at Cloudflare edge with infinite TTL, invalidated by programmatic purge. Three purge mechanisms are supported (see table below)
Rationale
Hotel content changes infrequently (a few times/day); edge cache eliminates 95%+ of origin requests; global performance
Trade-offs
Requires dependency graph for accurate purge; slightly stale content during purge window (seconds). “Purge All” causes temporary performance hit (cold cache) — should be used sparingly
Umbraco resolves dependency graph → identifies affected page URLs → purges only those URLs via Cloudflare API → warmup requests re-cache fresh pages
Code Deploy to PROD
CI/CD deploys new Next.js code (template, module, or component changes) without Umbraco content changes
Selective purge by tag/prefix
Deploy pipeline identifies which modules/templates changed → purges pages that use those components via cache tags or URL prefix purge. If change scope is too broad (e.g., layout-level change affecting all pages), falls back to “Purge All”
Emergency / Full Reset
Manual action by Tech Lead or admin
Purge All
”Purge All” button in Umbraco custom dashboard (with confirmation dialog) → clears entire Cloudflare zone cache → all pages served fresh from origin on next request. Used as last resort or after major deploy
Code-only deploys: When a production deploy contains only frontend code changes (e.g., a CSS fix in a module, a new component variant, a bug fix in rendering logic), the cached HTML at Cloudflare is stale because it was rendered by the previous code version. The CI/CD pipeline must trigger a selective cache purge as part of the deploy step. The scope of the purge depends on what changed — a single module change may only require purging pages that use that module, while a layout change requires a full purge.
Use Server-Side Rendering (SSR) as default rendering strategy, not Static Site Generation (SSG)
Rationale
8 sites × multiple languages × hundreds of pages = too many pre-built pages; SSR with edge cache achieves same performance; dynamic multi-site routing is simpler with SSR
Trade-offs
Requires running Next.js server (not static export); cold start on cache miss
Use proxy.ts instead of middleware.ts for network-level request handling (site resolution, header injection)
Rationale
Next.js 16 deprecates middleware.ts in favor of proxy.ts to establish a clearer network boundary within the Node.js runtime. This aligns with the new architecture of separating proxy-level logic from application logic
Trade-offs
Breaking change from Next.js 15 patterns; all middleware examples and docs need to reference proxy.ts
ADR-008: Explicit Caching with use cache Directive
Use Next.js 16’s explicit use cache directive instead of implicit caching behaviors
Rationale
Next.js 16 shifts to opt-in caching — dynamic code executes at request time by default. This aligns with our edge cache strategy: Next.js serves fresh content on each request, Cloudflare handles the caching layer. Explicit use cache can be used for expensive computations or shared data that doesn’t change per-request
Trade-offs
Must be intentional about what to cache at application level vs. edge level