What leaves your server
Evolve runs experiments inside your Next.js app. Visitors' exposures and events are written to your own Postgres (an evolve schema). Evolve Cloud never connects to that database: it sends your app a signed request, your app runs the counting query itself and answers with totals. This page lists everything your app sends to Evolve Cloud, field by field, and everything Evolve fetches.
Never leaves your server
- Visitor ids (the evolve_id cookie) or user ids
- Raw events, exposures or page views
- IP addresses, user agents, referrers of your visitors
- Event props and their values (only totals of goal values)
- Your local-fn goal functions
- Database credentials (EVOLVE_DATABASE_URL stays in your env)
Manifest (configuration)
Your app's Evolve configuration: which routes are tested, your goal definitions, versions and the public URL Evolve calls back on.
- How: Your app → POST /v1/manifest, with your secret key
- When: When the app boots, and on npx next-evolve push
- Kept: Stored on the site (routes, goals, versions, app URL) until you delete the site.
| Field | What it is |
|---|---|
pv | Protocol version. |
sdkVersion | next-evolve version. |
protocol.supported | Protocol versions the SDK speaks. |
nextVersion | Next.js version. |
routes | Route paths that hold optimizable regions (keys). |
routes{}.mode | "static" or "dynamic" rendering. |
goals | Goal keys from evolve.config.ts. |
goals{}.kind | "event" or "pageview". |
goals{}.event | Event name the goal counts (event goals). |
goals{}.source | Optional event source label (event goals). |
goals{}.where | Filters on event props as written in your config (literal strings, numbers, booleans). Don't put personal data in goal filters. |
goals{}.path | Page path or prefix (pageview goals). |
goals{}.value | How a value is read: none, a prop path, or a local function. |
goals{}.value.kind | "prop" or "local-fn" (the function itself never leaves). |
goals{}.value.path | Dotted prop path, e.g. order.total. |
goals{}.window | Attribution window, e.g. 7d. |
goals{}.unit | "visitor" or "user": the unit counted. Not an id. |
goals{}.direction | "increase" or "decrease". |
hash | Hash of routes + goals. |
appUrl | Your app's public base URL (webhooks and aggregate requests). |
Region schemas and control copy
What a region can change and what it shows today, so the dashboard and AI can propose variants.
- How: Your app → POST /v1/regions, with your secret key
- When: The first time each <Optimizable> region renders on a page (and when its schema changes)
- Kept: Stored per region until you delete the site.
| Field | What it is |
|---|---|
pv | Protocol version. |
id | Region id you chose (e.g. hero). |
jsonSchema | JSON Schema of the region's editable props (from your Zod schema). |
schemaHash | Hash of that schema. |
controlProps | The control copy: the current values of the schema's props, as rendered. Public page content, not visitor data. |
path | The page path the region rendered on. |
vars | Names of the per-page placeholders the region offers there, e.g. name or city (the route's params and its vars prop's keys). Names only, never their values. |
Aggregate answers (sufficient statistics)
Your app runs the counting query on its own Postgres and returns totals per experiment, variant, goal, segment and day. No row is about one visitor.
- How: Your app → the signed answer to Evolve's signed request to <appUrl>/api/evolve/aggregate
- When: Every few minutes to daily while an experiment runs (your plan's cadence), and on Recompute
- Kept: Stored as daily buckets and computed results until you delete the site.
| Field | What it is |
|---|---|
pv | Protocol version. |
requestId | Echo of Evolve's request id. |
site | Your site id. |
asOf | The cut-off time of the counts. |
sdk | next-evolve version. |
mode | "incremental" or "full". |
durationMs | How long the query took. |
partial | Whether the time budget ran out. |
remaining | Experiment ids not answered this time. |
rows[].exp | Experiment id. |
rows[].var | Variant id. |
rows[].goal | Goal key. |
rows[].seg | Segment (all traffic, or a rule id). |
rows[].day | Cohort day (UTC). |
rows[].n | Visitors exposed (a count). |
rows[].c | Visitors who converted (a count). |
rows[].s1 | Sum of goal values. |
rows[].s2 | Sum of squared goal values. |
rows[].mn | Exposed visitors whose goal window has passed (a count). |
rows[].mc | Of those, conversions (a count). |
rows[].ms1 | Their sum of values. |
rows[].ms2 | Their sum of squared values. |
usage[].day | Day (UTC). |
usage[].testedPageviews | Tested page views that day (a count, for plan usage). |
errors[].exp | Experiment a query failed for. |
errors[].goal | Goal a query failed for. |
errors[].message | The error text (at most 500 characters), e.g. a timeout or a missing table. Never row data. |
vitals[].exp | Experiment id (Core Web Vitals). |
vitals[].var | Variant id. |
vitals[].metric | lcp, cls or inp. |
vitals[].day | Day (UTC). |
vitals[].bins | Histogram: how many page views fell in each fixed range. Counts only. |
pages[].exp | Experiment id (pattern-route experiments only). |
pages[].n | How many distinct pages exposed it (a count, for “pooled across N pages”). Never which pages. |
App URL
Where Evolve sends sync webhooks and aggregate requests.
- How: npx next-evolve init → PUT /v1/site/app-url, with your secret key
- When: Once, at setup (the manifest can also carry it)
- Kept: Stored on the site.
| Field | What it is |
|---|---|
appUrl | Your app's public base URL. |
Bundle polling and CLI checks
These calls carry your secret key and no body: Evolve sends your signed experiment bundle back. Evolve records when the bundle was last fetched.
- How: Your app → GET /v1/bundle; the CLI → GET /v1/site, POST /v1/site/test, POST /v1/aggregates/request
- When: Every 60 s per app instance, and when you run CLI commands
- Kept: Last fetch time, and status codes of recent calls (Connect page).
Public page text (for AI suggestions)
Evolve fetches your public page like any visitor, as EvolveBot: no cookies, no login, and your SDK serves it control and records nothing. It keeps the visible text, at most 4,000 characters.
- How: Evolve → GET <appUrl><path>, anonymously
- When: When you ask for AI suggestions or a brand profile (cached 24 h)
- Kept: The page text and fetch time per path, until refreshed or the site is deleted. Prompts built from it are kept as AI run records.
What comes back to your app
- The signed bundle (GET /v1/bundle): which experiments run on which routes, each variant's props and weights. Your SDK verifies Evolve's signature.
- Sync webhooks (POST <appUrl>/api/evolve/sync): "a new bundle is ready", HMAC-signed.
- Aggregate requests (POST <appUrl>/api/evolve/aggregate): which experiments, variants and goals to count, HMAC-signed.
Retention and deletion
- In your Postgres: yours to set.
retentionDaysinevolve.config.tsdrops old events locally; Evolve can't see or delete them. - In Evolve Cloud: kept while the site exists. Delete site (Site → Settings) stops the site at once: your app gets an empty bundle and serves control, the key stops working, and all of the site's cloud data is deleted within 7 days. A record that the site was deleted (id, who, when, rows removed) stays with your org.
- Request metadata: our hosting provider keeps standard request logs (your server's IP address, path, status) for a short period.
Support access (beta)
During the beta, Evolve support may access your account to help you, for example to look at a failing aggregate. Every such access is logged and visible in your audit log.