Skip to content

01 — General Architecture

PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs: 02_Infrastructure_and_Environments.md, 03_MultiSite_and_Domains.md, A02_API_Contracts.md


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.


The platform is a headless CMS architecture built on three main layers:

LayerTechnologyResponsibility
Edge / CDNCloudflare (Pro)Global distribution, HTML caching, SSL termination, DDoS protection, programmatic cache purge
PresentationNext.js 16 (App Router + Turbopack)Server-side rendering, routing, theming, module rendering, explicit caching via use cache directive
Content ManagementUmbraco 17 (.NET 10 LTS)Content editing, Content Delivery API, media management, multi-site content tree, webhooks
DataAzure SQL + Blob StorageRelational content storage, media file storage
External ServicesSynxis, Navarino, GA4, Mailjet/SendGridBooking engine, calendar widget, analytics, transactional email

External Services

Data Layer — Azure

CMS Layer — Azure Web App

Presentation Layer — Azure Web App

Edge Layer — Cloudflare

Client Layer

HTTPS

Cache Miss

REST API

On Publish: Purge URLs

Widget Embed

Redirect

Events

Form Submissions

Browser / Device

DNS + SSL

WAF / DDoS Protection

Edge Cache — HTML + Assets

Next.js 16 (Turbopack)

SSR / RSC Rendering

Theme Engine

Module Renderer

Umbraco 17 Backoffice

Content Delivery API

Webhook Dispatcher

Custom Dashboard + Purge All

Azure SQL Database

Azure Blob Storage

Synxis Booking Engine

Navarino Bookpoint

Google Analytics 4

Mailjet / SendGrid (SMTP)


Cloudflare sits in front of all public traffic. Its responsibilities:

FeatureDescription
DNSAll 8 site domains point to Cloudflare, which proxies to Azure
SSLFull (Strict) mode, end-to-end HTTPS
HTML CacheAggressive 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 PurgeProgrammatic purge via Cloudflare API, triggered by Umbraco publish webhooks
WAFWeb Application Firewall rules for bot protection and rate limiting
Page RulesPer-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.

The Next.js application handles all frontend rendering:

AspectDetail
FrameworkNext.js 16 with App Router + Turbopack (default bundler for dev and prod)
RenderingServer-Side Rendering (SSR) as default; explicit caching via use cache directive (opt-in)
RoutingDynamic routes based on Umbraco content tree, multi-site routing via domain/path detection. proxy.ts replaces middleware.ts for network-level concerns
Data FetchingReact Server Components fetch from Umbraco Content Delivery API at request time (cache miss)
ThemingCSS variables per site loaded at layout level, driven by site identifier
ModulesCMS-driven page composition — modules are React components mapped to Umbraco Element Types
StorybookSeparate deployment for isolated component development and visual testing
BuildTurbopack for both dev and production builds (2–5× faster builds, 10× faster HMR). Webpack fallback available via next dev --webpack if needed
UmbracoNextJSCloudflareBrowserUmbracoNextJSCloudflareBrowseralt[Cache HIT][Cache MISS]GET /pt/hotel/roomsCached HTML (200)Forward requestGET /umbraco/delivery/api/v2/content/item/pt/hotel/roomsJSON content + modulesResolve theme, render modulesHTML response + cache headersHTML (200) + cache

Umbraco serves as the headless CMS with the following configuration:

AspectDetail
VersionUmbraco 17 LTS on .NET 10 LTS
ModeHeadless (Content Delivery API enabled)
Multi-siteSingle installation, 8 root nodes in content tree
APIBuilt-in Content Delivery API v2 (REST)
LanguagesMulti-language variants per content node
WebhooksNative webhook support for publish/unpublish events
BackofficeRestricted access (IP whitelist + MFA)
Custom DashboardEditorial metrics, cache status, SEO alerts, Purge All cache button (with confirmation dialog)
NextJSCloudflareWebhookUmbracoEditorNextJSCloudflareWebhookUmbracoEditorPublish content nodeResolve dependency graphTrigger webhook with affected URLsPOST /zones/{zone}/purge_cache (URLs)200 OKWarmup request (GET affected URLs)Fetch fresh contentNew HTML cached at edge
ServicePurposeDetails
Azure SQLContent databaseSingle database instance for all 8 sites; Umbraco manages schema
Azure Blob StorageMedia storageImages, documents, videos uploaded via Umbraco; served via CDN
ServicePurposeIntegration Type
Synxis (be.synxis.com)Booking engineRedirect with query string (hotel ID, dates, guests, locale)
Navarino BookpointCalendar date picker widgetJavaScript embed (bookpoint.js)
Google Analytics 4Site analyticsClient-side via GTM
Google Tag ManagerTag managementClient-side container
Mailjet / SendGridTransactional email (form submissions, notifications)External SMTP service, server-side integration from Umbraco

ADR-001: Headless CMS over Traditional Rendering

Section titled “ADR-001: Headless CMS over Traditional Rendering”
DecisionUse Umbraco in headless mode with Next.js as the presentation layer
RationaleEnables independent frontend/backend scaling; modern frontend tooling (React, SSR); edge caching; multi-site theming; better developer experience for AI-assisted development
Trade-offsAdditional complexity in deployment; requires API contracts; preview mode needs custom implementation

ADR-002: Single Umbraco Instance for All Sites

Section titled “ADR-002: Single Umbraco Instance for All Sites”
DecisionAll 8 websites share one Umbraco installation with separate root nodes
RationaleShared content (e.g., group info), single backoffice for editors, consistent Content Types, reduced maintenance
Trade-offsRisk of content tree complexity; requires clear governance; all sites share same deployment cycle

ADR-003: Cloudflare Edge Cache as Primary Cache

Section titled “ADR-003: Cloudflare Edge Cache as Primary Cache”
DecisionCache SSR output at Cloudflare edge with infinite TTL, invalidated by programmatic purge. Three purge mechanisms are supported (see table below)
RationaleHotel content changes infrequently (a few times/day); edge cache eliminates 95%+ of origin requests; global performance
Trade-offsRequires dependency graph for accurate purge; slightly stale content during purge window (seconds). “Purge All” causes temporary performance hit (cold cache) — should be used sparingly
ScenarioTriggerPurge TypeDetail
Content PublishEditor publishes/unpublishes content in UmbracoSelective URL purgeUmbraco resolves dependency graph → identifies affected page URLs → purges only those URLs via Cloudflare API → warmup requests re-cache fresh pages
Code Deploy to PRODCI/CD deploys new Next.js code (template, module, or component changes) without Umbraco content changesSelective purge by tag/prefixDeploy 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 ResetManual action by Tech Lead or adminPurge 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.

DecisionUse Server-Side Rendering (SSR) as default rendering strategy, not Static Site Generation (SSG)
Rationale8 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-offsRequires running Next.js server (not static export); cold start on cache miss

ADR-005: Booking Engine as External Redirect

Section titled “ADR-005: Booking Engine as External Redirect”
DecisionBooking flow redirects to external Synxis URL instead of embedded iframe or in-site booking
RationaleSynxis is PCI-compliant and handles payment; simplifies scope; Navarino widget provides date selection UX; each hotel has unique Synxis config
Trade-offsUser leaves the site for final booking; limited control over booking UX
DecisionFrontend, Storybook, and shared code live in a monorepo (Turborepo)
RationaleShared components/modules across 8 sites; single version of theming system; atomic changes across packages
Trade-offsBuild complexity; CI needs to handle selective builds

ADR-007: proxy.ts Replaces middleware.ts (Next.js 16)

Section titled “ADR-007: proxy.ts Replaces middleware.ts (Next.js 16)”
DecisionUse proxy.ts instead of middleware.ts for network-level request handling (site resolution, header injection)
RationaleNext.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-offsBreaking change from Next.js 15 patterns; all middleware examples and docs need to reference proxy.ts

ADR-008: Explicit Caching with use cache Directive

Section titled “ADR-008: Explicit Caching with use cache Directive”
DecisionUse Next.js 16’s explicit use cache directive instead of implicit caching behaviors
RationaleNext.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-offsMust be intentional about what to cache at application level vs. edge level

┌─────────────────────────────────────────────────────────────┐
│ CLOUDFLARE EDGE │
│ DNS → WAF → Cache (HTML + static assets) │
│ Purge API ← Umbraco Webhooks │
├─────────────────────────────────────────────────────────────┤
│ NEXT.JS (Azure Web App) │
│ ┌──────────┐ ┌───────────┐ ┌───────────┐ │
│ │ App │ │ Module │ │ Theme │ │
│ │ Router │ │ Renderer │ │ Engine │ │
│ │ (Pages) │ │ (Dynamic) │ │ (CSS Vars)│ │
│ └────┬─────┘ └────┬──────┘ └───────────┘ │
│ │ │ │
│ ┌────┴──────────────┴──────┐ │
│ │ CMS Client (API calls) │ │
│ └────────────┬─────────────┘ │
├───────────────┼─────────────────────────────────────────────┤
│ │ UMBRACO 17 (Azure Web App) │
│ ┌────────────┴─────────────┐ ┌──────────────────┐ │
│ │ Content Delivery API v2 │ │ Backoffice UI │ │
│ │ (Read-only, public) │ │ (Restricted) │ │
│ └────────────┬─────────────┘ └──────┬───────────┘ │
│ │ │ │
│ ┌────────────┴────────────────────────┴──────┐ │
│ │ Umbraco Core (.NET 10 LTS) │ │
│ │ Content Types · Webhooks · Media │ │
│ └────────────┬───────────────────┬───────────┘ │
├───────────────┼───────────────────┼─────────────────────────┤
│ Azure SQL Azure Blob │
└─────────────────────────────────────────────────────────────┘

ConcernApproachDocument
AuthenticationBackoffice only (no public user auth)15_Security.md
CachingEdge cache (Cloudflare) + server-side data cache (Next.js use cache)09_Cache_and_Performance.md
LoggingApplication Insights (Azure) for both Next.js and Umbraco02_Infrastructure_and_Environments.md
Error HandlingCustom error pages per site (404, 500); Sentry/App Insights for tracking04_Frontend_Architecture.md
ObservabilityAzure Monitor dashboards, Cloudflare analytics, uptime checks16_Analytics_and_Monitoring.md
Multi-languageUmbraco language variants + Next.js i18n routing10_MultiLanguage_and_i18n.md
SEODynamic meta tags, JSON-LD, sitemaps per site11_SEO_and_Metadata.md
GDPRCookie consent, form data handling, data retention15_Security.md

CategoryTechnologyVersion
Frontend FrameworkNext.js16.x
BundlerTurbopackBuilt-in (default)
ReactReact19.x
LanguageTypeScript5.x
CMSUmbraco17 LTS
CMS Runtime.NET10 LTS
DatabaseAzure SQLManaged
StorageAzure Blob Storage
CDNCloudflarePro
Component DevStorybook8.x
MonorepoTurborepoLatest
Package Managerpnpm9.x
Email ServiceMailjet or SendGrid
CSSBEM + SASS + CSS Variables
TestingVitest + Playwright + ChromaticLatest
CI/CDAzure DevOps / GitHub Actions
AI QA AgentopenClaw (Agentic AI)Local Mac runner
MonitoringAzure Application Insights
Node.jsNode.js20.9+ (required by Next.js 16)
AnalyticsGoogle Analytics 4 + GTM

  • Architecture diagram is validated by all team leads
  • All ADRs are reviewed and approved
  • Component boundaries are clear — each layer can be developed and deployed independently
  • API contract between Next.js and Umbraco is defined (see A02_API_Contracts.md)
  • Webhook → Purge flow is documented with sequence diagram
  • All external service integrations are identified and documented

Next document: 02_Infrastructure_and_Environments.md