Scaffold the zone app

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

bash
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.

ts
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:

  1. basePath is built from the slugs. Next.js prepends it to every route, asset URL, <Link> href, and router.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.
  2. assetPrefix is a path, not a URL, and is scoped by (orgSlug, agentSlug) to avoid collisions between agents that share a slug across orgs. A cross-origin assetPrefix breaks hydration silently.
  3. serverActions.allowedOrigins must 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:

VariableExamplePurpose
ZONE_ORG_SLUGacmeOrg slug the zone is bound to. Read at build time.
ZONE_AGENT_SLUGintake-botAgent 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_URLhttps://platform.example.comBase URL for outbound platform calls.
PLATFORM_ORIGINhttps://platform.example.comUsed 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.