Next.js feature flags

You have completed the Next.js quickstart and can sign in. This guide targets a feature flag on a CDP subject attribute and renders it server-side: Part 1 sets the flag and its keys up in the dashboard, Part 2 wires the app and watches the rendered output flip from OFF to ON after an ingest call.

Part 1 — set up the flag

1. Create the flag

From Feature flags in the dashboard, click Create flag. Fill in:

  • Key — e.g. beta-feature. Can't be changed after creation.
  • Description — optional.
  • Rollout percentage — leave at 0; the targeting rule below decides who sees it, not the rollout.
  • Make sure Enabled is checked.

Confirm with Create flag.

2. Add a targeting rule

On the flag's row, click Targeting, then Add rule. The new rule starts with one empty condition — set its attribute to plan, its operator to equals, and its value to beta. Set the rule's Outcome to On, then Save.

This flag now resolves to true for any subject whose plan attribute equals beta, and to false for everyone else.

3. Mint the evaluate key

Still on Feature flags, expand Test with curl and click Mint evaluate key, then confirm with Mint key. Copy the value shown in the Evaluate key minted dialog — an ff_-prefixed key, shown once — then confirm I have saved my evaluate key.

4. Mint the CDP ingest key

Open CDP in the dashboard and click Generate ingest key. Copy the value shown in the Ingest key minted dialog — a cdp_-prefixed key, also shown once — then confirm I have saved my ingest key.

Part 2 — wire up the app

5. Add the two env vars

npx timonier-nextjs init does not scaffold these — add them to .env.local yourself. These are two similar-looking opaque keys minted two steps apart, so check the prefix as you copy: the evaluate key (step 3) starts ff_, the ingest key (step 4) starts cdp_.

TIMONIER_FF_EVALUATE_KEY=<the ff_ evaluate key from step 3>
TIMONIER_FF_OFREP_URL=<your feature-flags data-plane origin>

TIMONIER_FF_OFREP_URL must include a scheme (https:// or http://) — a bare hostname fails at config load with ConfigError: TIMONIER_FF_OFREP_URL must include a scheme (https:// or http://); got: <raw>, naming the missing scheme rather than crashing unexplained. It's also the feature-flags data plane (e.g. https://feature-flags.<your-domain>), not the dashboard's control-plane API host (something like feature-flags.api.<your-domain>), which won't serve OFREP.

6. Render the flag

Add a page inside your Next.js project's App Router directory, e.g. app/beta/page.tsx:

import { evaluateFlag } from "@timonier/nextjs/rsc";

export const dynamic = "force-dynamic";

export default async function Page() {
  const enabled = await evaluateFlag("beta-feature", {
    targetingKey: "demo-subject",
  });
  return <main><h1>Beta feature: {enabled ? "ON" : "OFF"}</h1></main>;
}

export const dynamic = "force-dynamic" is load-bearing — without it Next caches the render and the flag looks like it never flips.

targetingKey is not your signed-in user's ID — it's an opaque key you choose. Use the exact same string as the subject_key you ingest in step 7; that's what links the two.

Visit http://localhost:3000/beta. You should see Beta feature: OFF — nothing has been ingested yet.

7. Ingest the attribute

The ingest key is not publishable like the evaluate key: it authorises writing and erasing a project's subject data, so treat it as a secret. Pass it by variable rather than pasting it into the command, which would leave it in your shell history.

read -rs CDP_INGEST_KEY   # paste the cdp_ key from step 4; it is not echoed
export CDP_INGEST_KEY

curl -i -X POST <your-cdp-data-origin>/v1/internal/subjects \
  -H "Authorization: Bearer $CDP_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject_key": "demo-subject", "attributes": {"plan": "beta"}}'

-i prints the status line, since a successful ingest returns 204 with no body — without it the command looks like it did nothing. HTTP/1.1 204 No Content means the attribute was accepted.

8. Reload until it flips

Ingest returns as soon as the attribute is accepted — projecting it into the flag's targeting happens asynchronously, so the very next reload can still show OFF. Reload the page; if it's still off, wait a moment and reload again. Once it flips, you'll see Beta feature: ON — server-rendered output that changed because of the attribute you just ingested.

API reference

This guide covers one evaluateFlag call. For the rest of the /rsc surface — the typed accessors and error handling — read the README that ships inside the installed package: node_modules/@timonier/nextjs/README.md.