Register the zone

Registering a zone is the one onboarding step with no admin UI. It is a code change in the platform's own source plus a redeploy, so unless you maintain the platform, this is a request you make rather than a step you perform.

Everything else about an agent can be configured by a platform admin in the browser. This cannot, because the registry is read at build time to generate the asset proxy.

To make the request, send the maintainer the four values below — they are everything needed, and supplying them together avoids a round trip:

ExampleNotes
Org slugacmeMust already exist
Agent slugrevenue-chatMust match the agent record exactly
Audiencecxpa-revenue-chatYou choose it; convention is cxpa-<agent-slug>
Zone URLhttps://acme-revenue-chat.vercel.appDeploy first, no trailing slash

Ask them to confirm two things when it is done: that the platform was redeployed without build cache, and whether the agent record already existed — that determines whether you get a zone JWT secret or have to rotate for one (step 4).

Nothing in this page blocks local development. See Local development modes — two of the three need no platform at all.

1. Add the registry entry

The platform's zone registry maps org slug → agent slug → zone config. It lives in the platform's source, not in a UI surface, so this step goes through whoever maintains the platform:

ts
acme: {
  'revenue-chat': {
    audience: 'cxpa-revenue-chat',
    url: process.env.ZONE_REVENUE_CHAT_URL,
  },
},

audience is the JWT audience claim the platform signs with and the zone verifies against. It must equal the zone's AUDIENCE_ID exactly. It is not derived from anything — pick a stable string and use it in both places. The convention is cxpa-<agent-slug>.

url is the zone's origin, read from an environment variable so it can differ per platform environment. A falsy value disables the zone: the platform stops proxying and the agent falls back to the built-in JSON editor. That is the intended way to turn a zone off.

The keys must match the org and agent slugs exactly — they are what the incoming request path is matched against.

2. Set the URL variable

ZONE_REVENUE_CHAT_URL=https://acme-revenue-chat.vercel.app

Locally that points at your zone dev server instead:

ZONE_REVENUE_CHAT_URL=http://localhost:4000

No trailing slash. A stray / produces a doubled path in the proxy destination and a redirect loop whose response body is unhelpful text/plain.

The variable name is not derived from the slug — it is written by hand in the registry entry — so choose something readable and keep it consistent across environments.

3. Redeploy the platform without build cache

The platform generates one asset rewrite per registered zone, mapping /assets/{orgSlug}/{agentSlug}/_next/* to the zone. Those rewrites are computed at build time. Adding the URL variable without a rebuild means the page proxies but every stylesheet and script 404s — a zone that renders as unstyled HTML.

On Vercel: Deployments → … → Redeploy, and uncheck "Use existing Build Cache".

4. Hand the secret to the zone

If the agent record was created after the registry entry existed, its zone JWT secret already exists. Read it at /acme/revenue-chat/zone-jwt-secret and set it in the zone as ZONE_JWT_SECRET.

If the page says Not set — rotate to generate, the registry entry came later. Press Rotate Secret; that generates it. This is the recovery path mentioned in Create the agent, and it is harmless — nothing else depends on the secret yet.

Rotating later is also fine, but it is a two-step change: rotate on the platform, then redeploy the zone with the new value. Between those two steps every request to the zone 401s.

5. The complete zone environment

VariableValueNeeded at
ZONE_ORG_SLUGacme — must match the registry keyBuild time
ZONE_AGENT_SLUGrevenue-chat — must match the registry keyBuild time
AUDIENCE_IDcxpa-revenue-chat — must match the registry audienceRequest time
ZONE_JWT_SECRETFrom /acme/revenue-chat/zone-jwt-secretRequest time
PLATFORM_ORIGINThe platform's origin, no trailing slashBuild time
PLATFORM_API_BASE_URLThe platform's origin, no trailing slashRequest time

The first two are compiled into the build, so changing them requires a rebuild, not just a restart.

Each of the first four has a distinct failure mode, which makes debugging easier than it looks:

Wrong valueSymptom
ZONE_ORG_SLUG / ZONE_AGENT_SLUGBuilds cleanly, then every request 404s
AUDIENCE_ID401 on every request — signature valid, audience rejected
ZONE_JWT_SECRET401 on every request — signature invalid
PLATFORM_ORIGINPages render, server actions fail as cross-origin

6. Order of operations

The dependency chain is real, and doing these out of order is the most common way a first zone deployment goes wrong:

  1. Deploy the zone and note its production URL. It will 401 everything until the platform is wired up — that is correct, not a failure.
  2. Add the registry entry and set ZONE_REVENUE_CHAT_URL on the platform.
  3. Redeploy the platform without build cache.
  4. Create the agent — this is when the zone JWT secret is minted. (If the agent already existed, rotate the secret instead.)
  5. Set ZONE_JWT_SECRET and AUDIENCE_ID on the zone and redeploy it.

Steps 2 and 4 are the pair that matters: the registry entry must exist before the agent record is created, or step 4 mints nothing.

7. Local development modes

There are three, and only the third needs the platform. If you do not have the platform running locally — which most zone authors will not — the first two are your loop.

Mode 1 — bypass the token (no platform)

Set DISABLE_ZONE_AUTH=true and DEV_AGENT_ID in web/.env.local, run the zone on its own port, and browse it directly at http://localhost:4000/acme/revenue-chat/app. The middleware waves every request through and getZoneSession() returns a fabricated session. This is the normal day-to-day loop: it exercises your pages, server actions and database code, and needs nothing else running.

What it does not exercise is authentication — see the warning in Build the zone app.

Mode 2 — prove the middleware fires (no platform)

With DISABLE_ZONE_AUTH unset, run the zone and check it rejects unauthenticated traffic:

bash
curl -i http://localhost:4000/acme/revenue-chat/app
# → 401 (no zone token)

curl -i -H "x-zone-token: garbage" http://localhost:4000/acme/revenue-chat/app
# → 401 (invalid signature)

curl -i http://localhost:4000/
# → 404 (outside the base path — the matcher only fires inside it)

Run this before every deployment. It takes seconds and it is what catches a middleware matcher that silently matches nothing — the failure that leaves a zone publicly readable without any error to notice.

Mode 3 — end to end through the platform (needs the platform)

Only possible if you can run the platform locally. Start both servers, with the platform's ZONE_REVENUE_CHAT_URL pointing at your zone:

bash
# terminal 1 — platform
npm run dev

# terminal 2 — zone
npm run dev -- -p 4000

Visit http://localhost:3000/acme/revenue-chat/app. The address bar stays on port 3000, the page body comes from port 4000, and assets are proxied. This is the only way to see a real minted JWT locally.

If you cannot run the platform, you are not blocked: modes 1 and 2 cover the code and the auth boundary respectively, and the first real token your zone sees will be on a deployed environment. Deploy to a preview and check it there.

8. Verify

  • GET https://agents.collectivexp.ai/acme/revenue-chat/app renders your zone.
  • In the network tab, _next/* requests come from the platform's origin, not the zone's. If they come from the zone's origin, assetPrefix is a URL instead of a path.
  • curl -i https://your-zone.vercel.app/acme/revenue-chat/app returns 401. If it returns 200, the middleware matcher is wrong and the zone is publicly readable.
  • The agent's sidebar shows an App link, which opens the zone in a new tab.

That third check is worth running on every deployment. It is the one that catches a zone that fails open.

Next

Run lifecycle and progress — showing the user what is happening.