Skip to content
Docsnext-evolve 0.9.0BlognpmSign in
Docs menu

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 use next-evolve 0.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)

  1. Open vercel.com/marketplace/neon, click Install, create a database and connect it to your project.

  2. Vercel adds the connection string to the project's environment variables (DATABASE_URL, plus POSTGRES_URL and others).

  3. Pull them into your app so init can find them:

    npx vercel env pull .env.local
    

init then asks whether to use DATABASE_URL for Evolve. Say yes.

#Option B: Supabase

  1. Create a project at supabase.com/dashboard/new.
  2. 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.
  3. Pass it to init with --db, or paste it when init asks.

next-evolve turns prepared statements off, so transaction-mode poolers work.

#Option C: Neon

  1. Sign up at console.neon.tech/signup and create a project.
  2. Copy the pooled connection string (its host contains -pooler).
  3. Pass it to init with --db, or paste it when init asks.

#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: init still finishes, variants serve, but nothing is measured. Later, set EVOLVE_DATABASE_URL and run npx 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:

  1. Give it a name and keep the path.
  2. Choose A/B/n test (a fixed split) or Bandit (shifts traffic to the better variant as data arrives).
  3. Pick the Primary goal, for example signed_up.
  4. Change a prop for Variant B, for example the title.
  5. 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

  1. Set the EVOLVE_* variables from .env.local in your host's production environment. EVOLVE_SECRET_KEY must be there at build time too.

  2. Run npx next-evolve migrate against the production database if it's a different one.

  3. 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