Use this guide when server code uses @ninetailed/experience.js-node, SSR plugin helpers, ESR
helpers, or a manual server-to-browser handoff.
Legacy server code commonly uses NinetailedAPIClient, SSR plugin continuity, ntaid, or ESR
preflight helpers. The Optimization Node SDK is stateless: create one process-level SDK, bind each
incoming request with forRequest(), and let the app own cookies, consent, profile persistence,
request context, and caching.
Follow the Node SDK integration guide unless a Next.js adapter owns the route.
Gather these inputs:
NinetailedAPIClient.ntaid and any browser continuation logic.@contentful/optimization-node for non-Next server paths.Identify which server code owns each responsibility:
This inventory prevents one request from being evaluated twice or from losing the profile before browser takeover.
Create one ContentfulOptimization instance per process and call forRequest() for each incoming
request. Event methods live on the request-bound client, not on the singleton. The minimum legal
shape is forRequest({ consent }); a migration request usually adds locale, profile, and
eventContext. locale selects the request locale, profile carries the app-owned anonymous
profile ID, and eventContext carries URL, user-agent, referrer, query, and other page data the SDK
cannot infer from your server framework.
Accepted request-bound page() or identify() results carry the profile, selected optimizations,
and flag changes for that request. Consent-blocked events return blocked results or diagnostics
without throwing.
Use a framework SDK when available. For Next.js, prefer the App Router or Pages Router migration guide so the adapter owns request state, provider handoff, page-event dedupe, and cookie behavior.
For a manual Node/Web hybrid, the app owns the profile cookie. The Node SDK exports
ANONYMOUS_ID_COOKIE from @contentful/optimization-node/constants, and its value is
ctfl-opt-aid, but Node does not read, write, or clear cookies for you. Read the cookie from the
incoming request, pass it as forRequest({ profile: { id } }), write the returned profile ID only
when persistence consent allows it, and keep the cookie browser-readable if the Web SDK must
continue the same visitor.
Stop building legacy experience configuration arrays. Fetch a baseline Contentful entry with one
concrete locale and enough linked entries, then resolve it with the request's selected
optimizations. The singleton resolver has no ambient visitor state, so pass selections explicitly
or use the request-bound fetchOptimizedEntry() path after an accepted request-bound page() or
identify() call.
Do not cache personalized outputs across visitors. Raw Contentful baseline entries can follow your normal content-cache policy, but rendered personalized HTML, merge-tag values, and Experience responses are request-specific.
Verify the request boundary:
@ninetailed/experience.js-node, SSR plugin imports, ESR helper imports, and ntaid.| Symptom | Check |
|---|---|
| Server event methods are missing | Call forRequest() first; event methods live on the request-bound client. |
| Non-sticky interaction tracking throws | Bind a request profile ID or use the event flow that derives one before sending Insights interactions. |
| Browser takeover starts a different visitor | Persist and pass the target anonymous ID according to the Web or framework SDK guide. |
| Personalized HTML leaks between visitors | Remove shared caching around request-specific responses and rendered output. |