Skip to main content
Everything in React works in Next.js as-is. @trevosdk/nextjs adds the one thing only a server can do: resolve variants before the page renders, so first-time visitors never see the control version flash.
next is an optional peer dependency (>= 14).

1. Request hook: Proxy or Middleware

Sets a stable trevo_id cookie so the server and browser bucket the same visitor identically. Use the file and named export for your Next.js version: For Next.js 16 and newer:
For Next.js 14 and 15:
Create exactly one of these files. Never add both proxy.* and middleware.* to one app: Next.js treats that as a conflict. Put the file beside the app’s app or pages directory. That is <app-package>/proxy.ts when app/ or pages/ is at the root, or <app-package>/src/proxy.ts for src/app/ or src/pages/; use the equivalent middleware.ts path on Next.js 14/15. In a monorepo, <app-package> means the Next.js app directory, not the workspace root. If next.config.* defines custom pageExtensions, the request-hook filename must use one of those suffixes. For example, pageExtensions: ['page.tsx', 'page.ts'] requires proxy.page.ts on Next.js 16+ or middleware.page.ts on Next.js 14/15. Write the matcher out as a literal, as above. Next extracts export const config by static analysis and rejects imported identifiers, so importing a shared constant fails next dev outright and, in a production build, silently falls back to running the request hook on every request — static assets included.

Compose with auth or internationalization

An app can have only one Proxy or Middleware. If auth, internationalization, or another integration already owns it, wrap that handler instead of adding a second file:
On Next.js 14/15, put the same pattern in middleware.ts and export it as middleware:
Both helpers keep the existing response body and status, redirects and rewrites, response headers and cookies, and downstream request-header overrides. They then add Trevo’s identity and preview headers to that same result. The version-neutral withTrevo alias is also available for shared setup code. Without this, the server has no identity to bucket on for a first-time visitor.

2. Resolve on the server

In a server component on the page running the experiment:
getTrevoBootstrap() reads the trevo_id cookie and the API key from the environment, then resolves every experiment with the same deterministic hash the browser uses. The client starts with those assignments already in hand, so the first paint is correct. It fails soft: if the config fetch fails, it returns an empty map and the client resolves on its own as usual. Previews work through it too: the request hook forwards ?trevo_force=key:variant to the render as an x-trevo-force header, and getTrevoBootstrap() serves the forced variant instead of a real assignment, so a page you preview doesn’t flip on hydration. Without the request hook, pass forceVariants yourself. Forced renders record no exposure, and the browser stamps everything it sends during a preview so nothing from a QA tab reaches results. It takes options: experimentKeys fetches only the experiments the route needs, and revalidate controls how long the server-side config fetch is cached. A signed-in user’s id goes here too — see the rule below. (The provider itself emits one page_view event with path when it initialises — don’t add your own page_view call or visits count twice.)

Keeping most routes static

Reading a cookie forces a route to render dynamically. To confine that to the pages that need it, skip the provider-wide bootstrap and pass a single experiment instead:
initialVariant takes precedence over the provider’s bootstrap.

Pages Router

The same primitives work from getServerSideProps:
Pass bootstrap to <TrevoProvider> exactly as in the App Router example.

3. Track conversions as normal

For conversions that happen on your backend — payment webhooks especially — use @trevosdk/node instead. Those events cannot be lost to an ad blocker or a closed tab.

Other backends

The Next.js entry is a thin wrapper. @trevosdk/nextjs/server has no Next.js imports, so the same primitives run on any Node 18+ or edge runtime:
resolveExperiments() never records an exposure — the client does that when the variant actually renders. Never replace a missing cookie with an id generated only for this call: persist it on the response first, or the same visitor can be re-bucketed on every request.

The rule that keeps client and server agreeing

Both sides must bucket on the same identity at the same moment: the user id when the visitor is identified, otherwise the trevo_id cookie value. Anything else assigns one person different variants on the server and the client, which corrupts the experiment. For anonymous visitors the request hook plus getTrevoBootstrap() handle this. Once a visitor signs in, the browser buckets on their user id — pass it to the server too: await getTrevoBootstrap({ userId: session?.user.id }). Omit it and SSR buckets on the cookie while the client buckets on the user id: the variant flips on hydration. If you hand-roll with resolveExperiments(), all of this is your responsibility.