← Back to Projects

Case Study

Enterprise Pharma Web Platform

AEM Edge Delivery Services

Zero Trust security architecture, component acceleration framework, and multi-language governance for a European pharma corporate client.

Deloitte Digital · 2025-2026

Overview

Led the architecture and delivery of two corporate websites for a European pharma enterprise on AEM Edge Delivery Services with Universal Editor. The platform serves up to 29 languages across two brands from a shared codebase, with differentiated design per brand.

In addition to architecture and technical decisions, I led a team of 5 engineers — owning estimation, sprint planning, and task breakdown in Agile. Managed SIT and UAT cycles, and coordinated directly with the client from requirements through go-live, ensuring business needs were correctly translated into technical delivery at every sprint. Maintained commercial awareness throughout the engagement, balancing scope, quality, and timeline under enterprise delivery constraints.

Challenges

01

Preview environments were publicly accessible

Any .aem.page or .aem.live URL was reachable without authentication - unreleased content, work-in-progress layouts, and reserved assets were exposed to anyone with the link.

02

Vanilla JS on EDS scales poorly on multi-developer teams

EDS components are built in plain JavaScript. Without governance, each developer writes their own data extraction logic from AEM dialog fields - duplicated, inconsistent, unmaintainable across a team.

03

Two brands/sites, 29 languages, one repository

Shared codebase with differentiated design per brand/site. Content updates, design changes, and language rollouts needed to be independent without cross-site regressions.

04

Edge personalization gap

User-based content (private areas, dashboards) requires runtime data injection. Client-side fetching would expose API calls and degrade performance. A server-side approach without a dedicated application server was needed.

Solutions

Two independent security layers protecting all preview and stage environments.

Layer 1 - Cloudflare Zero Trust + Azure AD SSO

Users attempting to access any protected domain are redirected to Azure AD login. Upon successful authentication, Cloudflare Access issues a signed JWT injected into subsequent requests. The Cloudflare Worker validates the JWT cryptographically using JWKS before forwarding any request.

Layer 2 - AEM EDS Site Token

The Worker injects a secret Authorization header into every request forwarded to the AEM origin. Even if someone bypasses Cloudflare DNS entirely and contacts the AEM origin directly, the request is rejected with HTTP 401 without the correct token.

Why two layers

Each layer assumes the other can be bypassed. Zero Trust protects against unauthenticated users. The site token protects against direct origin access. Neither is sufficient alone.

Deployment approach

All Worker logic is version-controlled in a dedicated GitHub repository and deployed via Wrangler CLI - not through the Cloudflare dashboard. This provides full audit trail, peer review via pull requests, and reproducible deployments across environments.

User Browser
  → Cloudflare DNS (proxied CNAME)
    → Cloudflare Zero Trust (Azure AD JWT issued)
      → Cloudflare Worker (JWT validation + Site Token injection)
        → AEM EDS Origin (validates Site Token)

A set of three importable JavaScript utility files integrated into the EDS project. Every component developer imports the same utilities instead of writing component-specific data extraction logic.

The problem it solves

AEM EDS provides component data as deeply nested HTML - the output of the Universal Editor dialog. Without a framework, every developer writes their own querySelector logic to extract each field. On a five-person team, this produces five different patterns for the same problem.

How it works

Each AEM component has a JSON model defined in its dialog configuration - the canonical field map authored in Universal Editor. The framework reads this model at build time and uses it to drive automatic extraction at runtime. The developer calls a single function, passing the block element. The framework traverses the nested HTML, maps each node to its corresponding dialog field by position and type, and returns a plain object whose keys exactly match the component model.

// Without framework - each developer writes their own extraction
const title = block.querySelector(':scope > div:nth-child(1) > div').textContent
const image = block.querySelector(':scope > div:nth-child(2) > div > picture')
const cta   = block.querySelector(':scope > div:nth-child(3) > div > a')

// With framework - one call, keys match the dialog model exactly
const component = generateComponentObj(block)

// component is now a typed object:
// {
//   title:       "Hero headline text",
//   image:       <picture>,
//   cta:         { text: "Learn more", href: "/page" },
//   description: "Supporting copy...",
//   ...
// }

// Rendering uses the object directly - no selector knowledge required
block.innerHTML = `
  <h2>${component.title}</h2>
  <p>${component.description}</p>
  <a href="${component.cta.href}">${component.cta.text}</a>
`

Because the returned keys are derived from the component model, not hardcoded selectors, the object is self-documenting and stable across refactors. Adding a new dialog field extends the object automatically - no extraction code to update. Developers write only rendering logic and component-specific behavior.

Impact

  • Reduced component development effort by 40% and eliminated an entire category of bugs caused by inconsistent dialog field extraction.
  • Model-driven architecture means component updates driven by evolving business requirements require little to no data-extraction refactoring.

Two corporate websites, up to 29 languages, shared repository. Information architecture and overall strategy were designed so that:

  • Language-specific content ensured by automated tag translation and custom i18n-like logic.
  • Brand design differences handled at the component level - no separate codebases.
  • Content authors across 10+ teams work within a consistent authoring model.
  • Granular ACL-based permission model - content editable only by authorized team members, aligned with business governance.

Template and design system decisions reduced content editing time by 50% compared to the previous platform.

The gap

EDS has no native mechanism for injecting user-specific content at request time. Handling this client-side would expose API credentials and introduce a visible loading phase - both unacceptable for a pharma corporate site.

Proposed approach

A Cloudflare Worker acting as a lightweight SSR layer at the edge. On each request, the Worker intercepts the EDS HTML response, fetches the relevant user data from a database or external API, injects it directly into the markup, and forwards the complete response to the browser.

// Cloudflare Worker - edge personalization pattern
export default {
  async fetch(request, env) {
    const response = await fetch(request)           // fetch EDS page
    const html     = await response.text()

    const userId   = getUserIdFromCookie(request)
    const userData = await env.DB.get(userId)       // D1 / external API

    const personalized = injectUserData(html, userData)

    return new Response(personalized, response)
  }
}

Why it matters

  • No API calls exposed to the client - credentials stay server-side.
  • No additional application server - the Worker is the personalization layer.
  • Consistent with the existing Cloudflare-first architecture already in place.

Results

MetricResult
Preview environments protected6 (STAGE Live, STAGE Preview, PROD Preview × 2 brands/sites)
Languages managed29
Component development effort−40%
Unauthorized access incidents0 post-implementation

Stack

AEM Edge Delivery ServicesUniversal EditorCloudflare WorkersCloudflare Zero TrustAzure ADVanilla JSWrangler CLIGitHub