Project layout

Your integration is two independent applications: the agent code that runs on Trigger.dev, and the zone app that runs on Vercel. They have different dependencies, different build steps, and different deployment targets.

Because they are independent, two separate repositories are equally valid — nothing in the platform couples them, and separate repos give each side its own history, access control, and CI. Choose that if the two halves are owned by different teams, or if the zone is open to contributors who should not see the agent code.

This tutorial puts both in one repository for simplicity: a single clone to get started, one place for database migrations, and no need to keep shared input/output types in step across two version histories. Everything that follows applies either way — if you split them, web/ simply becomes its own repository root and the two .env files below live in separate repos.

1. Repository shape

acme-revenue/
├── package.json              # agents project (root)
├── trigger.config.ts
├── tsconfig.json
├── .env.local                # agent secrets for local dev — gitignored
├── src/
│   ├── trigger/              # ← Trigger.dev discovers tasks here
│   │   ├── revenue-chat-agent.ts
│   │   └── nightly-etl.ts
│   ├── credentials.ts        # platform credential resolution helper
│   ├── warehouse.ts          # your domain code
│   └── etl.ts
├── supabase/                 # your own database, if you have one
│   ├── config.toml
│   └── migrations/
└── web/                      # ← the zone app: a separate npm project
    ├── package.json
    ├── next.config.ts
    ├── .env.local            # zone config for local dev — gitignored
    └── src/
        ├── middleware.ts
        ├── app/
        └── lib/

The two projects are not npm workspaces. That is a deliberate simplification: the zone deploys to Vercel with web as its root directory, and Vercel's build is much easier to reason about when web/package.json is self-contained. The cost is that you run install twice:

bash
npm install
npm install --prefix web

Why not one project? The zone needs next and React; the agents need the Trigger.dev SDK, a database driver, and whatever your domain logic uses. Merging them means Trigger.dev bundles React into every task deploy and Vercel installs a Postgres driver it never calls.

2. The agents project

json
{
  "name": "acme-revenue",
  "private": true,
  "scripts": {
    "trigger:dev": "trigger dev",
    "trigger:deploy:staging": "trigger deploy --env staging",
    "trigger:deploy:prod": "trigger deploy --env prod"
  },
  "dependencies": {
    "@cxpa/sdk": "^0.8.0",
    "@trigger.dev/sdk": "4.4.6",
    "pg": "^8.13.3",
    "zod": "^3.25.76"
  },
  "devDependencies": {
    "@trigger.dev/build": "4.4.6",
    "trigger.dev": "^4.4.6",
    "tsx": "^4.21.0",
    "typescript": "^5.8.3"
  }
}

Pin @trigger.dev/sdk and trigger.dev to the same exact version — a mismatch between the SDK and the CLI produces deploy-time bundling errors that read like source errors.

3. trigger.config.ts

ts
import { defineConfig } from '@trigger.dev/sdk'

export default defineConfig({
  project: 'proj_xxxxxxxxxxxxxxxxxxxx',
  runtime: 'node',
  logLevel: 'log',
  maxDuration: 300,
  dirs: ['./src/trigger'],
  additionalFiles: ['./src/trigger/prompts/system.md'],
  retries: {
    enabledInDev: false,
    default: {
      maxAttempts: 3,
      minTimeoutInMs: 1000,
      maxTimeoutInMs: 10000,
      factor: 2,
      randomize: true,
    },
  },
})

Three fields deserve attention:

  • dirs is where tasks are discovered. Files outside it can be imported by tasks but are not themselves scanned for task definitions.
  • additionalFiles is easy to forget and fails only in production. Trigger.dev bundles your code, so any file you read at runtime — a prompt template, a SQL file, a static dataset — must be listed here or the deployed task throws ENOENT on a path that works perfectly in dev.
  • maxDuration here is the project default; each task can raise its own. Keep it aligned with the agent's Max run duration on the platform (covered in Create the agent) so the two systems agree about when a run is stuck.

4. Your own database

If your agents need real storage, they get their own database — the platform is not it. This project uses Supabase, managed with the CLI:

bash
npx supabase init
npx supabase start                    # local Postgres on :54322
npx supabase migration new add_chat
npx supabase db reset                 # re-apply all migrations locally
npx supabase link --project-ref <ref>
npx supabase db push                  # apply to the hosted project

Both the tasks and the zone app read this database, but through different clients: the tasks connect with a Postgres driver over DATABASE_URL, and the zone uses a Supabase client. That is fine — just keep migrations in one place, here, so there is a single source of truth for the schema.

5. Environment variables

Each project has its own .env.local, and neither is committed. Commit an example file instead so the next developer is not reverse-engineering the list from process.env references.

Agents project — .env.example:

# Your own infrastructure
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres

# CXP Agent Platform
CXPA_API_URL=http://localhost:3000
CXPA_REVENUE_CHAT_AGENT_API_KEY=
CXPA_REVENUE_CHAT_INGEST_API_KEY=
CXPA_NIGHTLY_ETL_AGENT_API_KEY=

Customer secrets are deliberately absent: every third-party credential comes from the platform at run time, so there is no third-party API key in this file. See Credentials.

Zone project — web/.env.example:

ZONE_ORG_SLUG=acme
ZONE_AGENT_SLUG=revenue-chat
ZONE_JWT_SECRET=
AUDIENCE_ID=cxpa-revenue-chat
PLATFORM_API_BASE_URL=http://localhost:3000
PLATFORM_ORIGIN=http://localhost:3000

# Local-only escape hatch — never set in a deployed environment
DISABLE_ZONE_AUTH=true
DEV_AGENT_ID=1

Two conventions worth adopting from the start:

One key per agent, named after the agent. CXPA_REVENUE_CHAT_AGENT_API_KEY rather than a shared CXPA_API_KEY. Keys are scoped per agent — agent A cannot resolve agent B's credentials — so a single variable cannot serve two tasks, and discovering that at deploy time is unpleasant.

No trailing slashes on any URL variable. A stray / on PLATFORM_ORIGIN or PLATFORM_API_BASE_URL produces doubled paths in proxied requests and a silent redirect loop.

Note that TRIGGER_SECRET_KEY is read by the Trigger.dev SDK and CLI directly — you never reference it in code, but it must be present wherever tasks are deployed or triggered. Which key you hold is what selects the environment; see Deploy and go live.

Next

The repository is ready. Now the platform side: Platform setup.