Quickstart
This takes an existing Next.js app to a live A/B test in about 30 minutes, including the database. You'll install next-evolve, connect a Postgres database, mark one component as testable, and start an experiment from the dashboard.
Before you start. You need an Evolve account (start a 30-day free trial, no card needed), a Next.js 15.5+ app on the App Router, Node 22.12+, and a host that runs a Node server (not
output: "export"). These steps usenext-evolve0.9.0.
| Step | Time |
|---|---|
| 1. Create a site | 2 min |
| 2. Get a Postgres database | 2 to 5 min |
3. Run init |
3 min |
| 4. Wrap your first component | 5 min |
| 5. Open the page | 1 min |
| 6. Create and start an experiment | 5 min |
| 7. See the variant and the results | 5 min |
| 8. Deploy | 5 min |
The site's Overview page in the dashboard has a Get set up checklist that ticks each step as Evolve sees it happen: the SDK connecting, Postgres answering, the region, the goal, the experiment and the first exposure.
#1. Create a site
Sign in at app.evolveab.com, enter a site name and click Create site. Copy the secret key (evk_live_…) and the webhook secret (whsec_…). They are shown only once. If you lose them, issue a new key on the site's Connect page.
#2. Get a Postgres database
Evolve writes exposures and goal events to your Postgres (14 or newer), in its own evolve schema. Evolve Cloud never connects to it; it only receives aggregate counts (what leaves your server). Pick one:
#Option A: Neon through the Vercel Marketplace (app on Vercel)
Open vercel.com/marketplace/neon, click Install, create a database and connect it to your project.
Vercel adds the connection string to the project's environment variables (
DATABASE_URL, plusPOSTGRES_URLand others).Pull them into your app so
initcan find them:npx vercel env pull .env.local
init then asks whether to use DATABASE_URL for Evolve. Say yes.
#Option B: Supabase
- Create a project at supabase.com/dashboard/new.
- Click Connect and copy a pooler connection string (Transaction pooler on serverless hosts like Vercel). It looks like
postgres://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres. - Pass it to
initwith--db, or paste it wheninitasks.
next-evolve turns prepared statements off, so transaction-mode poolers work.
#Option C: Neon
- Sign up at console.neon.tech/signup and create a project.
- Copy the pooled connection string (its host contains
-pooler). - Pass it to
initwith--db, or paste it wheninitasks.
#Option D: Reuse your app's database
Evolve keeps to its own evolve schema and never touches your tables, so your app's existing database is fine. If DATABASE_URL or POSTGRES_URL is set (in your shell or .env.local), init offers to use it. The database user needs permission to create a schema. Use the pooled URL on serverless hosts.
#For local development: Docker
docker run -d --name evolve-pg -e POSTGRES_PASSWORD=postgres -p 54320:5432 postgres:17
The URL is postgres://postgres:postgres@localhost:54320/postgres.
No database yet? You can skip it:
initstill finishes, variants serve, but nothing is measured. Later, setEVOLVE_DATABASE_URLand runnpx next-evolve migrate.
#3. Run init
In your app's directory:
npx next-evolve init --routes "/"
init asks for the secret key, the webhook secret and the database. You can pass them as flags instead:
npx next-evolve init \
--key evk_live_… \
--webhook-secret whsec_… \
--db postgres://… \
--routes "/"
--routes lists the pages you want to test, as exact paths (for example "/ /pricing"). Add --dry-run first to see the diff without changing anything. init is safe to re-run.
It installs next-evolve and zod, checks the key, migrates the database and writes these files:
| File | What it is |
|---|---|
evolve.config.ts |
The routes Evolve may test, and their mode |
evolve.goals.ts |
What counts as success (a sample signed_up goal) |
app/api/evolve/[...evolve]/route.ts |
The routes the beacon and the cloud call |
proxy.ts (middleware.ts on Next 15) |
Picks a variant per visitor, with no network call |
instrumentation.ts |
Registers goals and sends your config to the cloud on startup |
app/layout.tsx |
Adds <EvolveScript />, the beacon |
package.json |
"prebuild": "next-evolve pull" |
.env.local |
The four EVOLVE_* variables (it never overwrites an existing line) |
If you already have a proxy.ts or middleware.ts, or init can't edit your layout, it prints the lines to add.
Static or dynamic mode. By default each tested page moves to app/[evolve]/… and is cached once per variant (static mode). init keeps a page where it is (dynamic mode) when moving it isn't safe, and says why. With cacheComponents: true in next.config (new create-next-app projects have it), init uses dynamic mode; static mode works too, with a "use cache" page. See Concepts.
On Vercel, add --cdn-routing to serve repeat views from the CDN without running the proxy. See CDN routing for the trade-off.
#4. Wrap your first component
A region is a component Evolve may change.
Let wrap do it: npx next-evolve wrap /pricing finds the components the page renders, generates the Zod schema from their TypeScript props and rewrites the usage, showing the diff first and restoring every file if tsc fails. See Wrap components.
Or by hand: declare which props it may change with a Zod schema, next to the component:
// components/hero.tsx
import { optimizable } from "next-evolve";
import { z } from "zod";
const heroSchema = z.object({
title: z.string().min(3).max(60),
cta: z.string().min(2).max(24),
});
function Hero({ title, cta }: z.infer<typeof heroSchema>) {
return (
<section>
<h1>{title}</h1>
<a href="/signup" data-evolve-goal="signup">
{cta}
</a>
</section>
);
}
export default optimizable(Hero, heroSchema);
Render it with <Optimizable> on a tested page. The props you pass are the control, the version visitors see today:
import { Optimizable } from "next-evolve";
import Hero from "@/components/hero";
<Optimizable id="hero" component={Hero} props={{ title: "Ship faster", cta: "Start free" }} />;
With cacheComponents: true, wrap it in <Suspense> from react. Without it, next build fails with "uncached or runtime data … outside of <Suspense>".
data-evolve-goal="signup" fires the signup event on click (on a form: on submit). The sample goal signed_up in evolve.goals.ts counts it. More in Goals & attribution.
#5. Start the app and open the page
npm run dev
Open the page once. The region registers with the cloud and appears under Regions in the dashboard within a few seconds.
#6. Create and start an experiment
In the dashboard, go to Regions, find hero and click New experiment:
- Give it a name and keep the path.
- Choose A/B/n test (a fixed split) or Bandit (shifts traffic to the better variant as data arrives).
- Pick the Primary goal, for example
signed_up. - Change a prop for Variant B, for example the title.
- Click Save draft, then Start on the experiment page.
The dev server picks up the change within about 5 seconds (60 seconds in production, or at once through the sync webhook once deployed).
#7. See the variant and the results
Open the page in a private window: you get Control or Variant B (50/50) and keep it on reloads. Each new private window is a new visitor. The experiment page also has Preview links that show a variant without counting anything.
The exposure is written to your database. With the Docker database:
docker exec evolve-pg psql -U postgres -c "select * from evolve.exposures"
The cloud can't reach localhost, so in development you run the aggregation yourself. Aggregates only count data older than 2 minutes, so wait 2 minutes after the visit, then:
npx next-evolve aggregate --upload
The experiment page shows the visitor under Results. Once deployed, the cloud runs this on a schedule.
#8. Deploy
Set the
EVOLVE_*variables from.env.localin your host's production environment.EVOLVE_SECRET_KEYmust be there at build time too.Run
npx next-evolve migrateagainst the production database if it's a different one.Deploy, then tell the cloud where the app lives and test the connection:
npx next-evolve connect --test --app-url https://your-app.com npx next-evolve doctor
The site's Connect page shows the same status. If a firewall or bot protection sits in front of your app, let /api/evolve/* through (Production).
Server the cloud can't reach? On next start or Docker, set aggregate: { mode: "pull" } in evolve.config.ts: the app polls the cloud every 60 seconds and posts the signed results back, so only outbound HTTPS is needed (pull mode).
Keep the events table small. Set retentionDays: 180 (minimum 30) and the SDK deletes old local rows once a day. It never deletes data a running or paused experiment still needs. Unset, nothing is deleted (retention).
#Next steps
- Hand the dashboard to your marketing team: Marketer guide.
- Count revenue, not just clicks: Stripe recipe.
- Something not working? Run
npx next-evolve doctor, then see Troubleshooting.