Proxy
createExperimentProxy, composing with your own proxy through resolveExperiments and applyExperiments, and what the proxy writes.
The proxy assigns versions on every eligible request and rewrites whole-page tests. Import from @delmaredigital/payload-experiments/proxy.
Standalone
import { createExperimentProxy } from '@delmaredigital/payload-experiments/proxy'
import { proxyRuntime } from '@/experiments/proxyRuntime'
export const proxy = createExperimentProxy(proxyRuntime)Composing with your own proxy
Call resolveExperiments, forward its request headers on both the normal response and any rewrite, then call applyExperiments to attach cookies and headers:
import { NextResponse, type NextRequest } from 'next/server'
import { resolveExperiments, applyExperiments } from '@delmaredigital/payload-experiments/proxy'
import { proxyRuntime } from '@/experiments/proxyRuntime'
export async function proxy(request: NextRequest) {
// ...your own logic
const result = await resolveExperiments(request, proxyRuntime)
const options = { request: { headers: result.requestHeaders } }
const response = result.rewrite
? NextResponse.rewrite(result.rewrite, options)
: NextResponse.next(options)
return applyExperiments(response, result)
}result.rewrite is set when the path belongs to a whole-page test and the visitor's version lives at another address. The result type is exported as ExperimentsProxyResult.
What it does on each request
- Removes any inbound
x-xp-*headers, before any skip check, so a browser cannot inject them. - Skips admin, API, Next.js internal and asset paths.
- Reads consent, the signed
xp_idcookie (or mints a new one), andgetUnit. - Assigns each running or decided test the request is eligible for.
- Writes signed request headers for server code:
x-xp-assignments: the assignments, at most 1 KB.x-xp-request: method, path and search, and consent, at most 2048 bytes.
- For whole-page tests, returns a rewrite to the assigned page. Holdouts see the Original page.
A newly minted cookie is forwarded into the same request, so server code sees it immediately. Server readers verify both headers; unsigned or changed values are ignored and a safe fallback is used when context is missing.
Whole-page routing
- Requests to any target path of a live page test are rewritten to the assigned group's page.
- A live test targeting a path takes precedence over archived routing.
- After a winner is made permanent, losing paths keep rewriting to the winner while the archived test record exists, until Release pages.
- Decided winners remain subject to targeting.
- Archived page rewrites serve shipped content by path alone and record no exposure.
Assignment and exposure exclusions
Staff, bots matched by the built-in user-agent list, preview renders and HEAD requests still receive assignments, but those assignments have exposable: false and no exposures are recorded. With cookie.consent returning false, nobody is enrolled; the proxy serves identity-free fallback content. See Assignment and targeting.

