Going to production
#Checklist
Env vars. Set these in your host's production environment (they're in
.env.localafterinit). They're server-only; never prefix them withNEXT_PUBLIC_.Var EVOLVE_SECRET_KEYevk_live_…from the dashboardEVOLVE_WEBHOOK_SECRETwhsec_…from the dashboardEVOLVE_DATABASE_URLYour production Postgres (pooled URL on serverless) EVOLVE_TRACK_SECRETOnly if you use POST /api/evolve/trackEVOLVE_SECRET_KEYmust also be set at build time:prebuildrunsnext-evolve pull, which bakes the current bundle into the build so a fresh instance serves it before its first fetch.Migrations. Run
npx next-evolve migrateagainst the production database once, and again after upgradingnext-evolve. It's safe to run on every deploy, for example in the build command:next-evolve migrate && next build.App URL. The cloud has to know where your app lives to send the sync webhook and aggregate requests. On Vercel production it's detected (
VERCEL_PROJECT_PRODUCTION_URL); elsewhere setEVOLVE_APP_URL=https://your-app.com, or register it once:npx next-evolve connect --app-url https://your-app.comIt must be a public
httpsURL.Check. After deploying:
npx next-evolve connect --test # the cloud pings /api/evolve/sync and dry-runs /api/evolve/aggregate npx next-evolve doctor # everything elseThe site's Connect page in the dashboard shows the same status: manifest received, bundle fetched, sync webhook, aggregate route, last aggregate. Run connection test there does what
connect --testdoes.Firewalls and bot protection. Evolve Cloud calls
POST /api/evolve/syncandPOST /api/evolve/aggregateon your app from servers without fixed IP addresses. If you use a WAF, bot protection or "attack mode" (Vercel Firewall, Cloudflare, …), add a rule that lets/api/evolve/*through. The requests are authenticated by their HMAC signature, so the route doesn't need other protection. If the connection test fails with403or a challenge page, this is why.
#Hosts
Per-host setup (Docker standalone, Netlify, Cloudflare in front): hosting.md.
| Vercel | Vercel + Global Config | Node server / Docker (next start) |
|
|---|---|---|---|
| Fresh instance serves | the build-time bundle | the bundle in Global Config (waits ≤ 20 ms), then the build-time bundle | the disk cache, then the build-time bundle |
| Background refresh | poll the cloud every 60 s (± 10%) on traffic | read Global Config (5 s cache) | every 60 s, plus on traffic |
| When an experiment changes | the instance that gets the webhook updates at once; others within one poll interval | all instances within ~5 s | the instance that gets the webhook at once; others within one poll interval |
| Events while the DB is down | dropped (one retry) | dropped (one retry) | buffered, up to 10,000 rows |
"Within one poll interval" means up to ~66 s after an instance's last check, on its next request (that request is still served the old bundle; the fetch runs in the background).
An instance that sees an experiment or variant its bundle doesn't know asks the cloud at once instead of waiting for its poll (since 0.6.1). It sees one when a returning visitor's cookie names it (the proxy refreshes in the background) or when a beacon reports it (collect answers the beacon at once, then waits for the refresh after the response and counts the exposure and its vitals). That also covers a Global Config item that has fallen behind. Each instance makes at most one such fetch per 5 s, shared by the proxy, collect and the render path, so made-up ids can't turn into a cloud fetch each. A failed fetch (network or 5xx) is retried after 10 s instead of a full poll interval.
#Vercel + Global Config (recommended on Vercel)
Without it, the sync webhook reaches one instance and the rest catch up by polling. With a Global Config store, the sync route writes the bundle there and every instance sees it within ~5 s (each instance re-reads it at most every 5 s, on traffic). The write also happens when the instance that gets the webhook already had the version.
- Create a Global Config store and connect it to the project.
- Set
EVOLVE_GLOBAL_CONFIG(the store's connection string),EVOLVE_GLOBAL_CONFIG_ID(ecfg_…),EVOLVE_VERCEL_API_TOKEN(a Vercel access token that can write the store) and, for a team-owned store,EVOLVE_VERCEL_TEAM_ID.
Bundles read from the store are verified like any other. Without the token the store is read-only and the sync route logs so. A read-only store keeps whatever bundle was last written to it, so instances fall back to their 60 s cloud poll for changes (plus the on-demand refresh above). Set the token, or remove EVOLVE_GLOBAL_CONFIG.
#Node server / Docker
registerEvolve() (in instrumentation.ts) polls in the background and caches the bundle on disk in os.tmpdir(), or EVOLVE_CACHE_DIR. Mount a volume there if you want restarts to survive a cloud outage. The writer flushes buffered events on SIGTERM.
#Pull mode: servers the cloud can't reach
If the cloud can't call your app (a firewalled or private network, no public URL), set aggregate: { mode: "pull" } in evolve.config.ts (since 0.7). registerEvolve() then polls GET /v1/aggregate-requests every 60 s with your secret key, runs each request exactly like the aggregate route would (same signature check, runner, lock and budget), and posts the signed result to POST /v1/aggregates. Only outbound HTTPS to the cloud is needed. The cloud switches a site to pull as soon as it polls, and back to push (if it has an app URL) when it hasn't polled for 15 minutes. The Connect page shows the mode and the last poll. Pull needs a long-running Node server (next start, Docker); on serverless hosts the cloud must reach /api/evolve/aggregate (push, the default). The sync webhook still needs the app URL; without it instances pick up new bundles from their 60 s poll.
#CDNs
On Vercel the cache keys on the rewritten path, so each variant is cached separately. Elsewhere, the proxy sets Cache-Control: private, no-store on tested routes. If a CDN in front of your app ignores that and caches by URL, every visitor gets whichever variant was cached first. Don't cache tested routes at the CDN. Cloudflare cache rules, Netlify (beta) and Docker: hosting.md. doctor names the CDN it sees in front of your site and fails if a tested route came from its cache.
#Faster repeat views on Vercel (cdnRouting)
Any proxy on Vercel adds ~20 ms TTFB to the requests it runs on. With cdnRouting (opt-in, since 0.6) a returning visitor's views within maxAgeSeconds (default 60 s) skip the proxy and are served from the CDN at the no-proxy TTFB. The cost: a pause or auto-rollback can take up to maxAgeSeconds to reach a visitor who has the route cookie. Set it up with npx next-evolve init --cdn-routing; details and the trade-off in configuration.md. It pays off for visitors who come back to a tested page within the window (navigating back to /, reloads); a funnel where most traffic is first views barely changes.
#Serverless functions
The /api/evolve/[...evolve] route exports maxDuration = 60. Aggregation stops at 25 s by default (aggregate.maxMs) and returns a partial result the cloud follows up on.
#How often results update
The cloud asks your app for aggregates on a schedule set by your plan (every 5–60 minutes, plus a full recount daily; during the alpha the schedule may run only daily) and when you click Recompute (at most once a minute). Aggregates count data older than 2 minutes.
#Upgrading
Upgrade with your package manager, run npx next-evolve migrate, deploy. The SDK tells the cloud which protocol versions it speaks; the cloud supports the current and the previous one and shows "upgrade required" in the dashboard at least 90 days before dropping one.