Developer Demo

Optimizely One Platform

How Graph, SaaS CMS, DAM, the Content Source API, Feature Experimentation, Web Experimentation, ODP, recommendations and Mark AI compose into one integration - what each product is responsible for, where they hand off to each other, and the decisions that make the difference between products running side by side and products actually composing.

Key Things to Know#

  • →Graph is the only read API. CMS content, DAM asset metadata and external systems registered through the Content Source API all converge on it, so the app has one query language and one cache story instead of one integration per product.
  • →Experimentation happens before rendering. Decide the flag at the edge, then carry the winning variation forward on the request. Where you put it is the consequential choice: in the request path, each variation gets its own cache entry; in a header, it is simpler but every variation shares one entry unless the CDN is told to vary on it. That choice is the difference between personalisation you can afford at scale and personalisation you cannot.
  • →Client-side tools can still reach server-rendered output. Web Experimentation decides in the browser, but if it persists that decision the next request can act on it server-side - so the variation survives into a real server render with no flash of the original.
  • →ODP closes the loop. Behavioural events go out, audience membership comes back, and a segment is mapped to a Graph variation key. Data collected on one request changes the content served on the next.
  • →One profile, read by every product. Visitor id, decision attributes, audience membership and the content interests inferred from browsing belong in a single object rather than being re-derived per product. Composing it means reading per-visitor state, which makes a route dynamic - so anything that has to stay cached reads that profile from the browser instead of the server.
  • →Not every product reads the same profile. The recommendation products each build their own from their own tracker, and warehouse analytics ingests nothing at all - it queries an export in place. Assuming one shared visitor profile across the whole suite is the most common scoping mistake.

Architecture Diagram #

Content supply on the left, Graph as the hub, the application in the middle split by runtime, and the decisioning and data products on the right. Solid edges are the request path; dashed edges leave it - a decision made in the browser, or an export into systems you own.

CONTENT SUPPLYDELIVERY HUBAPPLICATION RUNTIMEDECISION + DATAMEASUREMENT + ANALYTICSone Next.js deployment, three runtimesOptimizely SaaS CMSauthoring · Visual BuilderManagement API · previewpublishes into GraphDAM / CMP Assetsasset metadata via Graphbinaries via CMP CDNon-the-fly resizeContent Source APIsystems that are not the CMSPIM · branches · quotesindexed as Graph typestyped fields · GeoPointMark AICMS MCP serverauthoring + review agentsdev-time todayOptimizely Graphone GraphQL read APIfor CMS · DAM meta ·external sourcessearch · facets · geovariation filter forpersonalised contentEdge MiddlewareFX decideAll at the edgeencodes the variation keyone stable ISR key per bucketCMS redirects · visitor idServer Components (RSC)"use cache" + cacheTagGraph variation filterODP segment read on /renders CMS compositionsClient / BrowserWX snippet (blocking)ODP zaius tagtag · interaction eventsFX browser datafileFeature Experimentationdatafile · 60s TTLdecides at the edge, on theserver and in the browserData Platform (ODP)events in: zaius tag andserver /v3/eventssegments out: /v3/graphqlaudiences → variation keyWeb Experimentationclient-side snippetshares the visitor cookieread before first paintContent Recommendationskeeps its own profileown tracker + content feedProduct Recommendationskeeps its own profileown tracker + catalog feedExperiment Resultsdecisions + conversions instats engine computes liftread via the Results APIData Warehousecustomer ownedSnowflake · BigQueryraw decision + event rowsOptimizely Analyticswarehouse-nativequeries the warehouseno data copyGraphQLcontentpublish webhook · revalidateTagdatafile · decideAlldecision readbefore first paintmb_* eventssegmentsvariation in the URLrendered HTMLdecisions · conversionsEnriched Events Exportqueries in place
CMS, DAM and external sources index into Graph
Next.js queries Graph (GraphQL)
Graph returns content
Publish webhook - revalidateTag drops the ISR entry
FX and WX decisions reach the app
WX decision read in the browser, same pageview
Behavioural events to ODP
ODP audiences back to the server as a variation key
Mark AI agents assist authoring (dev-time, via MCP)
Request flow inside the Next.js deployment
Decisions and conversions to experiment results
Leaves the request path - into systems you own
Solid border - integrated in this repo
Dashed border - reference architecture, not integrated

The Shared Visitor Profile #

The loops below all need the same thing: one answer to "who is this visitor" that every product agrees on. That is one object, composed from the readers that already existed rather than replacing any of them.

What it composes

One object holding the visitor id, the attribute set decisions are made with, whatever segment or persona signal you derive from browsing, the audiences the data platform says they qualify for, and the variation those audiences resolve to.

Compose it from the readers you already have rather than reimplementing them. The attribute set in particular must have exactly one definition: if the edge, the server and the browser each build their own, they will drift, and a visitor can be bucketed one way server-side and another in the browser.

Why cached pages read it from the browser

Composing the profile means reading per-visitor state, and reading per-visitor state on the server makes that route dynamic. In a CMS-driven site an editor can place a block on any page, so a block that reads the profile server-side can quietly take an arbitrary page out of the cache - and nobody finds out until the site gets slower.

The way out is to split it: render the shell from CMS fields on the server, and fetch the personalised part from the browser against an endpoint that is allowed to be dynamic. The page keeps its cache entry and the content still personalises.

Three ways in

  • Server, on a route that is already dynamic. Offer two depths: one that reads only local state and costs nothing, and one that adds the audience lookup, so callers are not forced to pay for a network round trip they do not need.
  • A dedicated endpoint, explicitly dynamic, for anything running on a cached page. Give it a way to bypass the audience cache, because a verification or debugging surface has to show current state rather than what was true five minutes ago.
  • Browser, through a small client-side reader that dedupes concurrent callers into one request per tab and refetches when a signal changes without a server round trip.

The Four Feedback Loops #

Boxes and arrows show what talks to what. These loops are the reason the stack is worth composing: each one carries a signal from one product back into another and changes what the next request renders.

Data to decision to content

Behaviour observed in the browser becomes audience membership, which becomes a variation key on the content query. The page changes because of what the visitor did earlier, and the loop closes: every event fired afterwards carries the variation that was served, so the same stream that personalised the page also measures it.

Interaction event

browser tag

Behavioural profile

data platform

Audience

segment query

Variation key

one shared string

Content filter

allow the original

Personalised page

server render

Variation on events

attribution closes

Publish to invalidate

Nothing polls. Publishing is what makes the cache drop, and a time-based TTL is only the ceiling for a webhook that never arrived.

Editor publishes

authoring UI

Content reindexes

seconds, not instant

Webhook

content updated

Invalidate tags

every affected tag

Next request

rerenders once

Client decision to server render

The loop that makes a client-side tool safe on a statically cached site. The visual editor picks its variation in the browser before first paint, and the render it routes to is server-side and cached, so the content still comes from the CMS and there is no flash of the original.

Visual editor

decides in browser

Read pre-paint

same pageview, no storage

Validate at the edge

reject unknown keys

Carried to the server

path keys the cache

Cached variant

its own cache entry

Content meaning to recommendation

The content taxonomy stops being only a filing system and becomes a targeting vocabulary. The terms an editor tags an article with are the same strings the data platform builds an audience on and the content query filters by - so a personalised rail needs no new query and no schema change, because the filter was already there.

Content category

term on the article

Content viewed

event carries the terms

Top categories

accumulated per visitor

Customer attribute

targetable in the UI

Content filter

same terms, same query

Recommended rail

fetched client-side

Product by Product #

What each product is for, and what integrating it actually involves - the decisions that tend to matter, and the traps worth knowing before you scope the work.

Optimizely Graph

The single read API. Every content-shaped thing in the stack converges here: CMS pages, DAM asset metadata, and external systems registered through the Content Source API.

Query it server-side over GraphQL with a delivery key. The integration decision that matters is caching: queries whose shape is fixed and whose inputs come from a closed set can be cached and tagged for invalidation, while queries keyed on unbounded visitor input - search terms, coordinates - must stay uncached, or every request mints a cache entry nothing will ever read again.

SaaS CMS

Authoring, Visual Builder compositions, display templates, preview and the editorial workflow. Publishes sync into Graph rather than being read directly by the app.

Define content types in code and push them, so the model is version-controlled alongside the components that render it. Reads go through Graph, never the CMS directly. Writes - seeding, migrations, bulk edits - go through the Management API under service credentials. Preview is its own path: a short-lived token for editors in context, and app-level credentials where a draft link has to work for someone with no CMS login.

Feature Experimentation

Flags, A/B tests and server-side bucketing. The decision that matters most is made before rendering, so each variation can be cached independently.

The SDK decides from a locally cached datafile, so a decision costs no network call. Decide as early in the request as you can - at the edge, before rendering - and carry the result forward rather than deciding again per component. Suppress the decision event everywhere a flag is read for routing, and fire it once from whatever actually renders the variant, or a page that decides several times will over-count. The SDK can also fetch ODP segments itself, letting an audience carry a segment condition directly instead of you mapping segments to variations by hand; keep that lookup off the hot path, since unlike a datafile decision it is a real network round trip.

Data Platform (ODP)

The behavioural profile store. Events flow out of the app, come back as audience membership, and are mapped to a content variation so the next render is personalised.

Four channels, and they are worth separating. A browser tag for pageviews and interaction events. A server-side event call for things the browser should not be trusted with, like a form submission. A server-side query that reads back which audiences a visitor qualifies for, cached, because membership changes slowly. And customer-attribute writes, which are the ones that change who can use the platform: an event stream needs someone to write a segment rule, whereas an attribute carrying the visitor's content interests turns an audience into a dropdown a marketer can use unaided.

Web Experimentation

Visual, marketer-owned client-side testing on top of the same visitor identity as Feature Experimentation. Useful for changes that do not need a code deploy.

The snippet must load blocking in the head, or the original paints before the variation applies. That same blocking load is what connects it to server-rendered content: the decision exists before the body is parsed, so the page can read it synchronously and route the render to the matching CMS variation on the first pageview, with nothing stored. Check the experiment holdback when you read it, or the control sees the variation. Traffic should run both ways: push what you know about the visitor in as user attributes, and a marketer can target a content taxonomy term or a data-platform audience from the visual editor without a deploy. Audiences evaluate at page activation, so attributes pushed after the page loads apply from the next one.

DAM / CMP Assets

Asset library and delivery. Images arrive as references on content and are rendered with responsive srcsets and on-the-fly CDN resizing.

Delivery-side only is usually the right scope. Asset metadata rides along on the content reference from Graph, so no second lookup is needed, and the binary comes from the asset CDN with transform parameters applied per breakpoint. If you proxy those URLs to add caching or hide the origin, allow-list the hosts you will forward to rather than accepting arbitrary ones.

Content Source API

Brings systems that are not the CMS into Graph, so a PIM, a branch database or a quote feed can be queried with the same GraphQL and the same facets as CMS content.

Register a schema for the external system, then push rows into it. Once indexed they read back exactly like CMS content - same filters, same facets, same pagination - so a component does not need to know whether what it renders was authored or synced. This is the cheapest way to make a non-CMS system queryable without building a second delivery path.

Analytics and tag management

The analytics the customer already had. Not an Optimizely product, but the one every real stack has, and the place a marketer will ask to see experiment arms alongside everything else they measure.

Treat it as one more destination on the same event wrapper rather than a separate integration. The thing that makes it valuable is attribution: stamp each event with the variation the visitor was actually served, using the experiment-dimension convention the analytics tool already understands, and an Optimizely variation becomes a dimension in reports Optimizely does not own. Keep the tag bootstraps ahead of any tracking call so nothing fires into an undefined queue.

Mark AI

AI agents over the stack: content generation and review, SEO and GEO analysis, and natural-language authoring against the CMS.

Authoring-side rather than request-path. Agents reach the CMS and the experimentation platform through their published tool interfaces, driven from an editor or developer tool, so nothing here sits between a visitor and a response. That is the distinction to hold onto when scoping it: it accelerates the people producing the experience, and adds no runtime dependency to serving it.

Experiment Results

Where the experiment is actually measured. Every decision and every conversion lands here, and the stats engine turns them into lift and significance per variation.

Both event types should leave from where the visitor actually is, not from wherever the decision was computed - a decision event fired at the edge records an exposure nobody necessarily saw. Decide at the edge with the decision event suppressed, confirm what was rendered, then fire it from the client. Send conversions with the same attributes as the decision, or results cannot be segmented by the audiences that shaped them. Results are read in the console or pulled through the results API.

Optimizely Analytics

Warehouse-native product analytics: funnels, retention and cohort analysis over the same behavioural data, joined to whatever else the business already keeps in its warehouse.

The one product that does not ingest anything. An enriched export drops raw decision and conversion rows into storage the customer owns, that lands in their warehouse, and Analytics queries it in place rather than copying it. The value is that experiment results stop being a walled garden: a variation can be joined to revenue, churn or anything else already modelled there.

Content Recommendations

Picks the next best article or page per visitor from their behavioural profile, typically for a "recommended for you" rail.

The trap worth knowing before scoping it: it does not reuse the data-platform profile. It builds its own, from its own tracking script plus a crawl or feed of the content, and nothing syncs between the two stores. The most you can align is the visitor identifier. Integrating it means adding that script or its delivery API, then resolving the returned content keys through Graph so the rail renders with components you already have.

Product Recommendations

Catalog-driven merchandising: also-bought, trending and personalised product rails.

Needs a product catalog feed, and like Content Recommendations it keeps its own behavioural profile rather than reading the data platform - so an existing event stream cannot simply be piped in; the same signals have to be emitted to its tracker alongside the existing calls. Registering the catalog through the Content Source API first is what makes the returned products queryable next to everything else.

Request Lifecycle #

One request for a CMS page, from the edge to the rendered HTML, plus the invalidation that follows a publish. This is the path the diagram traces.

  1. 1

    Identify and decide, at the edge

    Establish the visitor id before anything else needs it, minting one on first request so the very first render is not anonymous. Apply CMS-managed redirects against the clean path, before any rewriting. Then decide flags from the cached datafile and encode the winning variations into the request itself. Two constraints worth designing in: only let flags that actually change CMS content reach this stage, and cap how many can stack, or the number of distinct cache entries multiplies with every flag you add.

  2. 2

    Resolve the route

    Split the variation keys back off the incoming path, and work out which content URLs to try - locale prefixes and trailing slashes both produce more than one candidate. This is also where you decide whether the request can stay cached: reading per-visitor state here makes the route dynamic, so scope that read to the few routes that genuinely need it rather than applying it globally.

  3. 3

    Query content

    Fetch by path with a variation filter, and always allow the original in the result set. Without that, a visitor who matches no variation gets nothing rather than the default page - the single most common way personalised routing produces a blank page in production. Keep a lookup fallback for the case where the path query misses.

  4. 4

    Render

    Dispatch the returned content type to a component and walk the composition tree. The variation that was actually rendered is what gets reported back as the decision event, from the client - not the variation that was decided upstream, which may never have been shown.

  5. 5

    Invalidate

    Nothing polls. Publishing is what drops the cache: the CMS reindexes and fires a webhook, and the handler invalidates every cache tag the content could be behind. A time-based TTL is the fallback ceiling for a missed webhook, not the mechanism.