Skip to content

Next.js Integration ​

This guide covers integrating @cognipeer/agent-server with Next.js App Router.

Installation ​

bash
npm install @cognipeer/agent-server

Basic Setup ​

Create a catch-all route handler:

typescript
// app/api/agents/[...path]/route.ts
import {
  createAgentServer,
  createInMemoryProvider,
  createNextRouteHandlers,
} from '@cognipeer/agent-server';

// Create storage (use PostgreSQL or MongoDB in production)
const storage = createInMemoryProvider();

// Create agent server
const agentServer = createAgentServer({
  basePath: '/api/agents',
  storage,
  allowAnonymousList: true, // a quick start with no sign-in: see "Who may list" in Request Context
  swagger: { enabled: true },
});

// Register agents
agentServer.registerCustomAgent('echo', {
  processMessage: async ({ message }) => ({
    content: `Echo: ${message}`,
  }),
}, {
  name: 'Echo Bot',
  description: 'A simple echo bot',
});

// Connect storage
await storage.connect();

// Export route handlers
export const { GET, POST, PATCH, DELETE, OPTIONS } = createNextRouteHandlers(agentServer);

With PostgreSQL ​

typescript
// lib/agent-server.ts
import {
  createAgentServer,
  createPostgresProvider,
} from '@cognipeer/agent-server';
import { createSmartAgent, fromLangchainModel } from '@cognipeer/agent-sdk';
import { ChatOpenAI } from '@langchain/openai';

// Create storage provider
export const storage = createPostgresProvider({
  connectionString: process.env.DATABASE_URL!,
});

// Create agent
const model = fromLangchainModel(new ChatOpenAI({
  modelName: 'gpt-4',
  openAIApiKey: process.env.OPENAI_API_KEY,
}));

const assistant = createSmartAgent({
  name: 'Assistant',
  model,
  tools: [],
});

// Create server
export const agentServer = createAgentServer({
  basePath: '/api/agents',
  storage,
  allowAnonymousList: true, // a quick start with no sign-in: see "Who may list" in Request Context
  swagger: { enabled: true },
});

// Register agent
agentServer.registerSDKAgent('assistant', assistant);

// Initialize storage
let initialized = false;

export async function initializeServer() {
  if (!initialized) {
    await storage.connect();
    initialized = true;
  }
}
typescript
// app/api/agents/[...path]/route.ts
import { createNextRouteHandlers } from '@cognipeer/agent-server';
import { agentServer, initializeServer } from '@/lib/agent-server';

await initializeServer();

export const { GET, POST, PATCH, DELETE, OPTIONS } = createNextRouteHandlers(agentServer);

With Authentication ​

typescript
// lib/agent-server.ts
import {
  createAgentServer,
  createPostgresProvider,
  createJWTAuthProvider,
} from '@cognipeer/agent-server';

const storage = createPostgresProvider({
  connectionString: process.env.DATABASE_URL!,
});

// Verifies each token's signature with this secret (HS256 by default). For an identity provider with
// rotating keys (JWKS), write an AuthProvider on a JWT library instead - see Authentication.
const authProvider = createJWTAuthProvider({
  secret: process.env.JWT_SECRET!,
  extractUserId: (payload) => payload.sub as string,
});

export const agentServer = createAgentServer({
  basePath: '/api/agents',
  storage,
  auth: {
    enabled: true,
    provider: authProvider,
    excludeRoutes: ['/docs', '/docs/*'],
  },
  swagger: { enabled: true },
});

Separate Agents Route ​

You can also create a separate route for agents only:

typescript
// app/api/agents/[[...path]]/route.ts
// Note: [[...path]] makes the path optional

import { createNextRouteHandlers } from '@cognipeer/agent-server';
import { agentServer, initializeServer } from '@/lib/agent-server';

await initializeServer();

export const { GET, POST, PATCH, DELETE, OPTIONS } = createNextRouteHandlers(agentServer);

Environment Variables ​

env
# .env.local
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
OPENAI_API_KEY=sk-...
JWT_SECRET=your-secret-key

Route Configuration ​

Configure routes in next.config.js if needed (Next.js 15 renamed this option to the top-level serverExternalPackages):

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ['@cognipeer/agent-server'],
  },
};

module.exports = nextConfig;

Using with Server Actions ​

typescript
// app/actions.ts
'use server';

import { storage, initializeServer } from '@/lib/agent-server';   // export the storage next to the server

export async function sendMessage(conversationId: string, message: string) {
  await initializeServer();
  
  // Use storage directly for server actions
  const result = await storage.createMessage({
    conversationId,
    role: 'user',
    content: message,
  });
  
  return result;
}

Client Component Integration ​

tsx
// app/chat/page.tsx
'use client';

import { Chat } from '@cognipeer/chat-ui';
import '@cognipeer/chat-ui/styles.css';

export default function ChatPage() {
  return (
    <div className="h-screen">
      <Chat
        baseUrl="/api/agents"
        agentId="assistant"
        theme="dark"
      />
    </div>
  );
}

A long-lived Node process ​

A turn runs inside the server process, independent of the HTTP request that started it (Runs), and the storage providers (pg, mongodb, better-sqlite3) are Node libraries. Run the app as a long-lived Node server (next start, a container) with export const runtime = 'nodejs' on the route. On a platform that freezes or stops the process once a response has ended, a turn that is still running can be cut off; the next server on the same database closes it as interrupted (run_interrupted).

Edge Runtime ​

For Edge Runtime, use a different approach:

typescript
// app/api/agents/[...path]/route.ts
import { createNextRouteHandlers } from '@cognipeer/agent-server';
import { agentServer } from '@/lib/agent-server';

// Note: Edge runtime has limitations with some storage providers
export const runtime = 'nodejs'; // Use Node.js runtime

export const { GET, POST, PATCH, DELETE, OPTIONS } = createNextRouteHandlers(agentServer);

Streaming Support ​

The Next.js adapter fully supports SSE streaming:

typescript
// Client request with streaming
const response = await fetch('/api/agents/conversations/123/messages', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    message: 'Hello!',
    stream: true,
  }),
});

// Handle SSE stream
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  const text = decoder.decode(value);
  // Parse SSE events
}

Uploading Files ​

The adapter authenticates a request before it reads its body, and then reads the body in pieces, refusing it with 413 as soon as it is over limits.maxBodySize (by default about 1.4 times the largest files.maxFileSize, plus 64 KiB; set the option when your agents are built per request, see Files). A file uploaded as itself (POST /files?name=... with the file's own Content-Type, see Files) arrives as bytes; JSON requests are parsed as before. Next.js and your host may have their own, smaller request size limits - make sure they allow your largest file.

CORS ​

cors in the server configuration is applied by the Next.js handlers to the OPTIONS answer only (the preflight). Origins are sent as one comma-joined value, so list one origin or "*"; answers to the other methods carry no CORS headers. For a chat on another origin add the headers in your Next.js configuration or middleware, or serve the chat from the same origin. See Embedding & limits.

Troubleshooting ​

SymptomCause and fix
404 Route not foundThe route file's folder does not match basePath (app/api/agents/[...path]/route.ts for /api/agents).
413 PAYLOAD_TOO_LARGE on an uploadThe body is over limits.maxBodySize; set it when your agents are built per request. Next.js and your host may also have request size limits of their own that are smaller than your files.
Streams arrive in lumpsA proxy in front of Next.js buffers the response. The Next.js adapter does not add X-Accel-Buffering: no (the Node and Express adapters do); turn buffering off for these routes on the proxy.
401 on every requestThe auth header is read as Authorization (or auth.headerName); middleware that strips it, or a missing resolveContext result, answers 401.
A turn ends as interrupted after a deployThe process was stopped mid-turn; see A long-lived Node process.

Error Handling ​

typescript
// app/api/agents/[...path]/route.ts
import { createNextRouteHandlers } from '@cognipeer/agent-server';
import { agentServer, initializeServer } from '@/lib/agent-server';
import { NextResponse } from 'next/server';

await initializeServer();

const handlers = createNextRouteHandlers(agentServer);

// Wrap with error handling if needed
export async function GET(request: Request, context: any) {
  try {
    return await handlers.GET(request, context);
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { error: { code: 'INTERNAL_ERROR', message: 'Internal Server Error' } },
      { status: 500 }
    );
  }
}

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub