Profile synchronization between client and server
Overview
Use this document to understand how the Optimization SDK Suite keeps a visitor profile continuous when a Node server and a browser client both participate in personalization and analytics. It explains the difference between profile identity and profile data, how each SDK runtime stores that state, and which application boundaries still belong to your implementation.
Runtime applicability
Profile synchronization applies differently across SDK runtimes:
The synchronization model
Profile synchronization is event-driven. The SDKs do not run a continuous replication protocol between server memory, browser memory, cookies, localStorage, and the Experience API.
Instead, synchronization uses three contracts:
- A shared profile identifier - The browser and server can share the
ctfl-opt-aidcookie, also exported asANONYMOUS_ID_COOKIE, so both runtimes send future Experience events to the same profile. - Experience API responses - Calls such as
page(),identify(),screen(),track(), and stickytrackView()return event results whosedatacontainsprofile,selectedOptimizations, andchangeswhen the event is accepted and data is available. - Runtime persistence - Stateful clients cache returned profile data for their own next startup when persistence consent allows. The Node SDK is stateless, so the application persists only the identifier or session state it needs between requests.
The Experience API remains the source of truth for profile aggregation. The shared cookie is a continuity handle, not a full profile record.
Constraints before synchronization
Profile synchronization depends on runtime policy and storage state before an SDK sends or resumes profile-changing events:
- Event consent and allowed event types - Gated events need event consent unless
allowedEventTypespermits that event when event consent is unset or false. - Persistence consent - Durable profile-continuity keys, platform profile caches, and
ctfl-opt-aidload or write only when persistence consent resolves totrue. Configured defaults can seed in-memory state without enabling durable storage. - Storage availability - Browser localStorage, readable cookies, AsyncStorage, UserDefaults, or SharedPreferences must be available for relaunch or client-server handoff continuity. Storage failure does not change the Experience API source of truth, but it can break local durability.
- Preview and preflight mode - Preview and preflight flows can evaluate profile state without storing normal profile mutations. Do not use them as the durability path for a profile ID you intend to continue.
- Offline behavior - Stateful clients can queue Experience events while offline, but no newer profile data is available until a request succeeds.
- Configured defaults -
defaults.profile,defaults.selectedOptimizations,defaults.changes, and consent defaults bootstrap one runtime. They do not synchronize server and client state unless both runtimes also use the same profile ID or the same Experience API response. In React Web provider handoff and manual Next.js provider or root handoff, keepdefaultsfor configuration or default state such as consent policy, and pass server-returned Optimization data throughserverOptimizationStatewhen the provider or root receives the data directly. In App Router bound components fromcreateNextjsAppRouterOptimization(), the bound server root or provider owns that handoff. In Pages Router, passprops.contentfulOptimization.serverOptimizationStatefromgetServerSideOptimizationProps()into the bound root or provider inpages/_app.tsx.
For consent gates, see Consent management in the Optimization SDK Suite. For state shape and observable state mechanics, see Core state management.
Runtime responsibilities
The server, browser, and API each own different parts of the profile lifecycle:
The React Web SDK uses the Web SDK under its providers and hooks, so profile synchronization follows the same browser mechanics.
React Native is also stateful, but it persists with AsyncStorage instead of browser cookies. It can continue a mobile profile through the Experience API, but this repository does not provide a built-in cookie handoff between a React Native app and a Node server.
Server-rendered profile data
Choose the server-rendering path before implementing a hybrid flow, because the choice determines
whether the browser needs only the shared profile ID or a bootstrapped OptimizationData snapshot.
The shared cookie is enough when the browser performs personalization after hydration. It is not enough when the server already rendered profile-derived HTML and the browser must continue from the same evaluated data before its first client-side Experience response.
Direct Web SDK bootstrapping must use the same OptimizationData response that drove the server
render:
If the browser re-resolves entries from stale localStorage while the server rendered from a newer
profile evaluation, the user can see a mismatched variant or profile-derived value. For direct Web
SDK initialization, use explicit defaults. App Router bound components hand off server
OptimizationData through the bound server root or provider. Pages Router bound components receive
the same data from pageProps in pages/_app.tsx. For React Web and manual Next.js provider or
root handoff, pass the server OptimizationData to serverOptimizationState when the provider or
root receives the data. A fresh client-side page() response or a render boundary can also prevent
stale cached state from driving visible content.
Personalized HTML is not shared-cache safe unless the cache varies on all personalization inputs. Raw Contentful entries are the safer cache boundary; resolve variants per request or per profile selection.
Profile identity and profile data
Profile identity and profile data are separate artifacts:
The full Profile object can contain traits, audience memberships, location data, and session
statistics. Treat it as profile data, not as a general client-visible session token. In a hybrid
browser and server application, the durable shared value is normally the profile ID.
Merge tags resolve from the active profile. They do not read values from changes.
Experience API response flow
All profile-changing Experience paths converge on upsertProfile():
- When no
profileIdis provided, the API client creates a profile withPOST /profiles. - When a
profileIdis provided, the API client updates that profile withPOST /profiles/:id. - The response is normalized to
OptimizationDatawithprofile,selectedOptimizations, andchanges, then surfaced as event resultdata.
The Experience API client also exposes getProfile(id) for reading the current evaluated profile
data without sending an event. That read path can refresh application state, but it does not replace
the event-driven synchronization flow used by page(), identify(), screen(), track(), and
sticky trackView().
Stateful SDKs write that response to in-memory signals as a single batch. This means subscribers see the profile, selected optimizations, and changes as one consistent snapshot after any state interceptors complete.
Web and React Web read browser storage during initialization when persistence consent allows it. After Core publishes in-memory state changes, Web signal effects mirror profile-continuity state to localStorage and the anonymous-ID cookie on a best-effort basis. Browser subscribers do not wait for a separate durable write before observing the new in-memory snapshot.
React Native reads AsyncStorage during async initialization when persistence consent allows it. Its state persistence interceptor awaits the AsyncStorage write for an accepted Experience response before Core publishes that response snapshot to observable state.
iOS and Android read platform storage before initializing the bridge when persistence consent allows
it. Their native bridge state handlers write the received snapshot through UserDefaults or
SharedPreferences when persistence consent is true, then update native observable state from that
snapshot.
Stateless Experience methods and trackView() return event results with accepted and optional
data. Non-sticky trackView() resolves with no data; trackClick(), trackHover(), and
trackFlagView() resolve without an event-result payload. Those Insights-only paths require a
request-bound profile because there is no ambient SDK state. The caller decides what to render, what
to persist, and what to pass into later SDK calls.
Server-side mechanics
The Node SDK extends the stateless Core runtime. Create the Node SDK once per process or module, but
bind request-scoped inputs with forRequest() before calling event methods.
Profile upsert behavior
In Node, Experience methods use the request-bound profile ID as the profile selector:
For the difference between the application Contentful locale and the SDK Experience/event locale, see Locale handling in the Optimization SDK Suite.
The Node SDK passes that ID to the Experience API as profileId. If the ID is absent, the API can
create a profile and return the new profile.id.
Insights-only stateless methods need a request-bound profile because there is no ambient state. For
example, non-sticky trackView(), trackClick(), trackHover(), and trackFlagView() require a
profile ID passed to forRequest(). Sticky trackView() first sends an Experience event and can
use the returned profile for the paired Insights event.
Request-scoped state
The server must derive these values per request:
- The current profile ID, usually from
ANONYMOUS_ID_COOKIEor an application session. - The consent decision, usually from an application-owned consent cookie, session, or preference.
- The known user ID and traits, when an authenticated user exists.
- Page context, locale, user agent, and optional request options such as IP override.
The Node SDK does not keep those values between requests. This avoids cross-request state leakage in long-lived Node processes and serverless runtimes.
Consent and persistence policy
Consent policy belongs to the application layer on the server. A conservative server policy is:
- When consent is unknown or denied, do not persist
profile.id. - When consent is unknown or denied, do not emit follow-up tracking events.
- When event consent is granted, pass the stored profile ID into Experience calls.
- When persistence consent is granted, persist the returned
profile.id. - When consent is revoked, clear the stored profile ID and stop sending events until consent is granted again.
If the server uses preflight: true, the Experience API evaluates a profile state without storing
the mutation. Use that for preview or evaluation flows, not as the normal continuity path for a
profile ID you intend to persist.
Browser-side mechanics
The Web SDK extends the stateful Core runtime. It owns browser-specific persistence and event
delivery around the same profile, selectedOptimizations, and changes signals described in
Core state management.
Initialization order
On construction, the Web SDK first resolves event consent and persistence consent. It loads durable
profile-continuity state and anonymous-ID cookies only when persistence consent resolves to true.
The effective initialization order is:
- Resolve consent and persistence consent from explicit
defaults, persistedLocalStorevalues, and legacy accepted consent. - Merge explicit
defaultssupplied in the SDK configuration. - Load persisted profile, changes, and selected optimizations from
LocalStoreonly when the resolved persistence consent istrue. Explicitdefaults.profile,defaults.changes, anddefaults.selectedOptimizationscan still seed in-memory state for that SDK instance. - Initialize Core state with the merged defaults.
- Read the current and legacy anonymous-ID cookies only when the resolved persistence consent is
true. - If a cookie ID exists and differs from
LocalStore.anonymousId, seed the anonymous ID from the cookie. The SDK clears active and cached profile-continuity state before seeding only when that cookie also differs from the activeprofile.id, where an active profile exists.
This order lets the browser resume from localStorage when durable profile-continuity persistence is allowed and there is no server handoff, while still letting a server-set cookie take precedence when a hybrid request identifies a different profile. If persistence consent is granted after construction, the Web SDK runs the cookie adoption check at that time.
Persistent browser state
The Web SDK exposes browser persistence through stable behavior, not localStorage key names.
Application code must treat exact keys as diagnostic-only implementation details and use SDK APIs,
configuration defaults, and ANONYMOUS_ID_COOKIE instead of reading or writing SDK localStorage
entries.
During diagnostics, you might see localStorage entries such as __ctfl_opt_consent__,
__ctfl_opt_persistence_consent__, __ctfl_opt_debug__, __ctfl_opt_anonymous_id__,
__ctfl_opt_profile__, __ctfl_opt_selected-optimizations__, and __ctfl_opt_changes__. These
names and serialized shapes are implementation details, not values application code reads or writes.
The app-personalization-consent name appears in examples only as an application-owned consent
cookie ('granted', 'denied', or absent) that server and browser code can read. It is not an SDK
localStorage key.
Structured cache values are schema-validated when they are read. Malformed JSON or invalid shapes are removed instead of being used as SDK state.
When persistence consent is true, the browser also persists the profile ID in the ctfl-opt-aid
cookie. This cookie is the only built-in browser-server synchronization channel.
Persistence consent controls whether the SDK writes and reloads profile-continuity state: anonymous
ID, profile, selected optimizations, changes, and the ctfl-opt-aid cookie. It does not disable SDK
storage of consent or debug settings.
Browser persistence is best-effort and follows Web SDK state. When persistence consent is true,
Web signal effects mirror profile, selected optimizations, changes, and anonymous ID to localStorage
and the readable cookie after Core state changes. Live reads come from memory; localStorage and the
cookie are startup and server-handoff durability channels.
Cookie and localStorage reconciliation
The Web SDK keeps the cookie and localStorage anonymous ID aligned when persistence consent is
true:
- During initialization, a server-set
ctfl-opt-aidcookie overrides a different local anonymous ID only when persistence consent resolves totrue. The SDK callsreset()to clear active and cached profile-continuity state only when the cookie also differs from the activeprofile.id, where an active profile exists. If the cookie matches the active profile, the SDK preserves active state and writes the cookie value to localStorage. If persistence consent is granted later, the same cookie adoption check runs then. - When the profile signal changes after an Experience response, the SDK writes the full profile to
localStorage and writes
profile.idto both localStorage and thectfl-opt-aidcookie. - When the profile signal is cleared and an anonymous ID already exists, the SDK removes cached
profile data but preserves the existing anonymous ID and
ctfl-opt-aidcookie for future continuity. Usereset()or withdraw persistence consent when the application needs to end that continuity. - When the profile signal is cleared and no anonymous ID exists, the SDK removes
ctfl-opt-aid. - If the legacy cookie
ntaidexists, the SDK migrates its value and removes the legacy cookie.
For a hybrid Node and browser application, do not mark ctfl-opt-aid as HttpOnly. The Web SDK
must be able to read the cookie during initialization. Server or Next.js response code owns
response-cookie policy such as SameSite, Secure, path, domain, and readability. Configure
those attributes so the server route and browser code can access the same cookie. The Web SDK
browser writer supports its cookie.domain and cookie.expires options, always writes Path=/,
and does not expose SameSite or Secure controls for the browser-side write.
Stateful event delivery
In the browser, Experience events select the profile with this priority:
getAnonymousId(), which by default returnsLocalStore.anonymousIdonly when persistence consent istrue.- The current in-memory
profile.id. - No profile ID, which lets the Experience API create a profile.
Stateful browser methods accept a profile field in some payload types for API consistency, but the
synchronization channel is the stateful profile ID selected by the queue. In normal Web SDK usage,
seed or override browser identity through the shared cookie, localStorage, defaults, or
getAnonymousId, not by trying to pass a one-off profile object to every stateful call.
Insights events in the browser use the current profile signal. If no profile exists, the Insights queue logs a warning and skips delivery.
Hybrid request lifecycle
A Node and Web SDK application usually follows this lifecycle:
- First server request. The request has no
ctfl-opt-aidcookie. If the application’s consent policy permits the server event, the server callspage()oridentify()without a profile ID. The Experience API creates or evaluates a profile. - Server response. The server persists the returned
profile.idinctfl-opt-aidwithpath: '/'and a readable cookie policy for browser code when the application’s persistence consent policy permits durable continuity. - Browser initialization. The Web SDK reads the cookie only when persistence consent resolves
to
true. If localStorage has a different anonymous ID or cached profile, the SDK clears the old profile-continuity state and adopts the cookie ID. - Browser events. Browser
page(), router events,identify(),track(), and stickytrackView()calls update the same Experience API profile. - Browser persistence. Each Experience response updates browser signals. It updates
localStorage and the
ctfl-opt-aidcookie only when persistence consent istrue. - Later server request. The server reads the same cookie and passes
{ profile: { id } }into the next stateless SDK call.
The node-sdk+web-sdk reference implementation demonstrates this flow by setting
ANONYMOUS_ID_COOKIE on the server and verifying that the browser localStorage anonymous ID matches
the cookie.
Known users and identify()
identify() adds a known userId and optional traits to the profile flow. It does not replace the
application’s authentication session. The user ID comes from your application identity system, while
the profile ID continues to select the Experience API profile being updated.
On the server, pass the current anonymous profile ID into the request-bound client. If the page view
must be attributed to the known user, call identify() before page() on that same client:
Then render and persist from the response that matches the user state you want for that response:
pageResult when the page response drives the render, or identifyResult when identification is
the last Experience response for the request. The request-bound client updates its internal profile
after each accepted Experience response, so the later page() call uses the profile returned by
identify(). If identify() and page() happen in different request-bound clients, bind the
returned profile into the later client with forRequest({ profile }), or persist profile.id and
bind { id } on the later request. Do not pass a profile into page().
If the request arrived anonymous but the response must include identified traits, render from the
identify() response or from a later page() response that used the identified request-bound
profile.
In the browser, call identify() after the Web SDK has adopted the shared anonymous ID. The
subsequent Experience response updates the same stateful profile and rewrites the browser caches.
Reset, revocation, and teardown
Different lifecycle methods have different synchronization effects:
consent(false) leaves the active in-memory SDK profile, selected optimizations, and changes
available until the application calls reset() or tears down the runtime. Future event calls emit
only when their event type remains permitted by allowedEventTypes. If consent revocation must also
remove active session personalization, call reset() in the browser and clear the server-side
cookie or session value in the same user flow.
Edge cases and failure modes
The following cases are common sources of profile-sync bugs:
Implementation checklist
Use this checklist when implementing a hybrid Node and browser profile flow:
- Persist
ANONYMOUS_ID_COOKIE(ctfl-opt-aid) on the server after an Experience response returnsprofile.idand the application’s persistence consent policy allows durable continuity. - Use
path: '/', an appropriate domain, and a browser-readable cookie policy when the Web SDK must continue the same profile. - Pass
{ profile: { id } }into Node SDK Experience calls when a cookie or session ID exists. - Call
identify()beforepage()on the same Node request-bound client when the page view must be attributed to a known user. - Let the Web SDK initialize once per browser runtime so it can reconcile cookie and localStorage state.
- Confirm persistence consent resolves to
truebefore expecting the Web SDK to load persisted profile-continuity state or adoptctfl-opt-aid. - Render from the
OptimizationDataresponse that matches the current identity state. - Bootstrap direct Web SDK
defaults, use App Router bound component handoff, pass Pages RouterpagePropsdata throughserverOptimizationState, or passserverOptimizationStatethrough React Web or manual Next.js provider or root handoff when server-rendered personalized output must match client-side resolution before the first browser Experience response. - Clear both browser state and server persistence when consent revocation must end profile continuity.
- Use single-locale CDA entry payloads for both manual
baselineEntryresolution and SDK-managedfetchOptimizedEntry(); avoidwithAllLocalesandlocale=*. - Cache raw Contentful delivery payloads, not profile-evaluated SDK responses or personalized HTML unless the cache key varies on the full personalization context.
Related docs and reference implementations
- Integrate the Optimization Node SDK in a Node app
- Integrate the Optimization Web SDK in a web app
- Integrate the React Web SDK in a React app
- Integrate the React Native SDK in a React Native app
- Integrate the Optimization Next.js SDK in a Next.js App Router app
- Integrate the Optimization Next.js SDK in a Next.js Pages Router app
- Core state management
- Consent management in the Optimization SDK Suite
- Interaction tracking in Node and stateless environments
- Interaction tracking in Web SDKs
- React Native SDK interaction tracking mechanics
- iOS SDK runtime and interaction mechanics
- Android SDK runtime and interaction mechanics
- Node SDK and Web SDK reference implementation
- Next.js App Router reference implementation
- Next.js Pages Router reference implementation