Local development
Run two dev servers, one per terminal.
Platform — set in the platform's .env.local:
ZONE_<AGENT_SLUG>_URL=http://localhost:4000
(env-var naming depends on the platform's zone-registry conventions — your platform admin will tell you the exact key)
Zone — .env.local in your zone repo:
ZONE_JWT_SECRET=<provided by platform admin>
AUDIENCE_ID=<provided by platform admin>
PLATFORM_API_BASE_URL=http://localhost:3000
PLATFORM_ORIGIN=http://localhost:3000
ZONE_ORG_SLUG=<your-local-org-slug>
ZONE_AGENT_SLUG=<your-local-agent-slug>
ZONE_ORG_SLUG + ZONE_AGENT_SLUG compose the zone's static basePath. They must match the (org, agent) pair the platform registry binds this zone to. Wrong values build cleanly but every request 404s.
Run:
# terminal 1 (platform)
npm run dev
# terminal 2 (zone) — non-default port
npm run dev -- -p 4000
Visit http://localhost:3000/{orgSlug}/{agentSlug}/app. The URL bar stays on :3000; the page body is served by :4000; assets are proxied via the platform's rewrites().
Smoke tests
curl -i http://localhost:4000/{orgSlug}/{agentSlug}/app
# → 401 (no zone token)
curl -i -H "x-zone-token: garbage" \
http://localhost:4000/{orgSlug}/{agentSlug}/app
# → 401 (invalid signature)
Then, in a browser: submit a run → run row appears in the platform DB → run detail polls → terminal status.
Deploy to Vercel
The zone is a second Vercel project (or a separate folder configured as one). Same Git repo or a sibling repo — either works; the important thing is its own Vercel project with its own production URL.
1. Create the project
- Push the zone repo to GitHub.
vercel linkin the zone repo → create a new Vercel project.- Set the deployment region to the same region as the platform project. Cross-region rewrites add 50–200 ms per request.
2. Environment variables
Set the same six env vars from .env.local in the Vercel project — with production values:
| Variable | Value |
|---|---|
ZONE_ORG_SLUG | matches the (orgSlug, agentSlug) pair registered on the platform |
ZONE_AGENT_SLUG | matches the (orgSlug, agentSlug) pair registered on the platform |
ZONE_JWT_SECRET | the per-agent secret from the platform UI (rotate by regenerating in the platform and redeploying with the new value) |
AUDIENCE_ID | matches the audience the platform mints with for this agent |
PLATFORM_API_BASE_URL | platform's production URL, no trailing slash |
PLATFORM_ORIGIN | platform's production URL, no trailing slash |
No trailing slashes on
PLATFORM_API_BASE_URL/PLATFORM_ORIGIN. A stray/produces//assets/...in the proxy destination and triggers a silent 308 loop with atext/plainbody — extremely confusing to debug.
3. Disable Vercel Skew Protection
Vercel → Settings → Advanced → Skew Protection → off on both the zone and the platform project.
With Skew Protection on, platform and zone ?dpl=<id> deployment identifiers collide during the asset proxy and every _next/* request returns ERR_TOO_MANY_REDIRECTS.
4. Deployment Protection
Vercel → Settings → Deployment Protection → Vercel Authentication: Only Preview Deployments (or off).
With "All Deployments" selected, proxied asset requests get redirected to Vercel's SSO page and loop. Password protection must also be off (or preview-only) on production.
The zone is already protected at the app layer — any request without a valid JWT gets a 401 from the zone's middleware. Vercel-level protection on production adds nothing and breaks the proxy.
5. Deploy order
- Deploy the zone first. Note its production URL.
- Ask the platform admin to register the zone's URL against the agent. The platform then knows how to proxy
/app/*traffic. - The platform's asset rewrite reads env vars at build time — when the platform admin adds the new URL, the platform must be redeployed without build cache (Deployments → … → Redeploy → uncheck "Use existing Build Cache") for the new rewrite to take effect.
6. Production smoke tests
GET https://platform.tld/{orgSlug}/{agentSlug}/app→ zone page renders inside the platform shell.- Network tab:
_next/*requests are served from the platform's origin (proxy), not the zone's. - Submit a run → run row appears → run detail polls → terminal status.
curl -i https://zone.tld/{orgSlug}/{agentSlug}/app→ 401 (zone is protected).- Same
runIdappears in both platform logs and zone logs.
Checklist
-
next.config.tshasbasePath,assetPrefix, andallowedOrigins. -
middleware.tscallscreateZoneMiddlewarewithsecretandaudience. - Pages and server actions read state via
getSession(). - Platform calls go through
createZoneClient— no directfetchto platform endpoints. - All six env vars are set in production (no trailing slashes).
- Skew Protection is off on both projects.
- Deployment Protection is off (or preview-only) on production.
- The platform admin has registered the zone URL and JWT secret for the agent.
- Platform redeployed without build cache after the URL env var was added.
You're done. Visit /{orgSlug}/{agentSlug}/app on the platform to see the zone live.