A zone app is a normal Next.js App Router project with three platform-specific config tweaks: basePath, assetPrefix, and serverActions.allowedOrigins.
1. Create the project
npx create-next-app@latest my-agent-zone \
--ts --app --eslint --tailwind --src-dir --import-alias "@/*"
cd my-agent-zone
npm install @cxpa/sdk
@cxpa/sdk/zone ships everything needed to verify the JWT, read the session, and call platform endpoints. jose is pulled in transitively — do not install it directly.
Do not add an auth library. A zone never authenticates users itself — identity arrives in the platform-signed JWT, and adding a second auth system (Supabase Auth, NextAuth, Clerk) creates a competing source of identity that the platform will not honour.
A database client is a different matter and is expected if the zone owns domain data — a list to render, records to write. Install whatever driver that database needs, use it server-side only, and keep it strictly separate from identity, which still comes from the session. See Zone data and tenancy.
2. Folder layout
The source tree is flat. basePath (configured below) is what makes the platform serve app/page.tsx at /{orgSlug}/{agentSlug}/app.
There is no app/api/ directory, and adding one will not work. A client-side fetch('/api/…') resolves against the platform's origin — the one in the browser's address bar — so the request never reaches the zone. Every client-to-server call goes through a server action instead.
my-agent-zone/
├── src/
│ ├── app/
│ │ ├── page.tsx landing surface (e.g. input form)
│ │ ├── runs/[runId]/page.tsx optional run detail
│ │ ├── layout.tsx
│ │ └── globals.css
│ ├── components/
│ ├── lib/
│ │ └── actions.ts 'use server' — triggerRun / pollRun / ...
│ └── middleware.ts
├── next.config.ts
├── .env.example
└── package.json
3. Configure next.config.ts
The zone is mounted under /{orgSlug}/{agentSlug}/app/* on the platform. basePath and assetPrefix ensure links and static assets resolve correctly behind the proxy.
import type { NextConfig } from 'next'
const orgSlug = process.env.ZONE_ORG_SLUG
const agentSlug = process.env.ZONE_AGENT_SLUG
if (!orgSlug || !agentSlug) {
throw new Error(
'ZONE_ORG_SLUG and ZONE_AGENT_SLUG must be set — they compose the zone basePath and assetPrefix.',
)
}
const nextConfig: NextConfig = {
basePath: `/${orgSlug}/${agentSlug}/app`,
assetPrefix: `/assets/${orgSlug}/${agentSlug}`,
experimental: {
serverActions: {
allowedOrigins: [process.env.PLATFORM_ORIGIN!],
},
},
}
export default nextConfig
Three points:
basePathis built from the slugs. Next.js prepends it to every route, asset URL,<Link>href, androuter.push()call — so the source tree stays flat. The slugs are read at build time; throwing on missing values is intentional, because a build with the wrong basePath silently 404s in production.assetPrefixis a path, not a URL, and is scoped by(orgSlug, agentSlug)to avoid collisions between agents that share a slug across orgs. A cross-originassetPrefixbreaks hydration silently.serverActions.allowedOriginsmust include the platform's origin. Server actions run on the zone but the browser POSTs them from the platform's host; without the allow-list Next.js rejects the request as cross-origin.
A single zone deployment binds to exactly one (orgSlug, agentSlug) pair. Build separate deployments for separate agents.
4. Environment variables
The zone needs the following at runtime:
| Variable | Example | Purpose |
|---|---|---|
ZONE_ORG_SLUG | acme | Org slug the zone is bound to. Read at build time. |
ZONE_AGENT_SLUG | intake-bot | Agent slug the zone is bound to. Read at build time. |
ZONE_JWT_SECRET | (provided by platform admin) | HS256 secret used to verify x-zone-token. |
AUDIENCE_ID | (provided by platform admin) | Audience claim the platform mints with. |
PLATFORM_API_BASE_URL | https://platform.example.com | Base URL for outbound platform calls. |
PLATFORM_ORIGIN | https://platform.example.com | Used by Server Actions allow-list. |
PLATFORM_API_BASE_URL and PLATFORM_ORIGIN are typically the same value. Strip trailing slashes from both — a stray / produces //assets/... in the proxy destination and triggers a 308 loop on Vercel.
5. Next steps
Continue with Auth to wire JWT verification.