Experiments with Next.js and on the server

Server Components

// app/layout.tsx
import { headers } from "next/headers";
import { getAssignments, X3Analytics } from "@x3growth/sdk/next";
import { X3Experiments } from "@x3growth/sdk/react";

export default async function Layout({ children }: { children: React.ReactNode }) {
  const assignments = await getAssignments({
    apiOrigin: process.env.X3_API_ORIGIN!,
    publishableKey: KEY,
    proxyKey: process.env.X3_PROXY_KEY!,
    headers: await headers(),
    path: "/",
  });
  return (
    <html>
      <body>
        <X3Analytics publishableKey={KEY} experiments={assignments} />
        <X3Experiments assignments={assignments}>{children}</X3Experiments>
      </body>
    </html>
  );
}
  • getAssignments posts the visitor's IP, user agent, host, path and – when present – the consented x3_vid cookie to POST /v1/x/{pk}/assign with the proxy key (scope ingest:proxy, server env only). It caches per visitor for 60 s and answers {} after 150 ms (everyone control, nothing exposed). getVariant(key, …) is the one-key form.
  • <X3Analytics experiments> inlines the arms (window.__x3xp) so the browser starts with them – no /v1/x request on that page load, exposures consistent with the server render.
  • Pages that render an arm must not be cached across visitors: render them dynamically or mark the response private.

First-party proxy

createX3ProxyHandler (route app/n3/[...path]/route.ts) forwards GET /v1/x/{pk} with its query. With consent (c=1) the HttpOnly x3_vid cookie travels server-to-server as x-3x-vid, so visitor-sticky experiments work without exposing the id to scripts.

Server helper

import { createX3Server } from "@x3growth/sdk/server";
const x3 = createX3Server({ apiUrl, apiKey: process.env.X3_SECRET_KEY!, publishableKey: KEY, proxyKey: process.env.X3_PROXY_KEY });

const arms = await x3.experiments.assign({ ip, ua, host: "www.example.com", path: "/pricing" });
await x3.leads.addNote({ idempotencyKey: archiveId }, "Ergänzung des Besuchers");
  • Outside Next.js and without a secret key, fetchAssignments({ apiOrigin, publishableKey, proxyKey, request }) from @x3growth/sdk/server makes the same call (any fetch-based server).
  • experiments.assign is the source of truth for prices and offers: never let the browser decide what a customer pays. Signed QA tokens (forceToken) are honoured; unsigned overrides are not.
  • leads.addNote attaches the visitor's follow-up free text to an existing lead – by lead id or by the Idempotency-Key used for leads.create – and retries for up to 60 s while the lead is still being created. It never creates a lead or a conversion. The text is personal data, readable only with leads:read (POST /v1/projects/{project}/leads/notes, scope leads:write).