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 install @supabase/*; zones never authenticate users themselves.
2. Folder layout
The source tree is flat. basePath (configured below) is what makes the platform serve app/page.tsx at /{orgSlug}/{agentSlug}/app.
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.