Skip to content

Quickstart

Mount a production chat agent in a Next.js App Router application. Conversations, attachments, knowledge, and memory are hosted for you. Identity stays on your server, where it belongs.

Three files change: a route, an env var, and the component you render.

Note

Prerequisites: Next.js 14/15/16, React 18 or 19, and an authentication solution you already trust — Clerk, Auth.js, Supabase Auth, or your own sessions. Tailwind is not required; the widget stylesheet is precompiled.

Want conversations in your own Postgres? Follow Bring your own database instead. Same widget, same handler, same security model — you supply the store.

1

Create an agent and copy its key

Create an account, add an agent, and publish it. Publishing is what gives the agent a model, a system prompt, a theme, and its enabled features.

Copy the agent's server key. It looks like mck_live_… and is read only on your server — never ship it to the browser.

code
.env.local
MORDN_CHAT_KEY="mck_live_..."
2

Install

code
npm install @mordn/chat-widget ai @ai-sdk/react

That is the entire dependency list. No database driver, no storage SDK, and no model-provider package — the published agent config supplies the model.

3

Add the route

code
app/api/chat/[[...chat]]/route.ts
import { createMordnHandler } from '@mordn/chat-widget/server';
import { auth } from '@clerk/nextjs/server';
 
export const { GET, POST, DELETE, OPTIONS } = createMordnHandler({
  apiKey: process.env.MORDN_CHAT_KEY!,
  getUserId: async () => (await auth()).userId,
});

apiKey and getUserId are the only required options. That one key wires persistence, private attachments, published configuration, knowledge retrieval, memory, hosted MCP tools, and feedback.

Swap the Clerk import for whatever you already use. The contract is the same: return a stable user id from a verified server session, or null.

code
Auth.js
import { auth } from '@/auth';
 
getUserId: async () => (await auth())?.user?.id ?? null,

Warning

Never derive this id from a header, query parameter, or request body. Those are controlled by the browser. getUserId is the entire authorization boundary — the store and storage are bound to whatever it returns. Return null when unauthenticated and the handler responds 401 before touching user data.

4

Mount the widget

code
components/assistant.tsx
'use client';
 
import { ChatWidget } from '@mordn/chat-widget';
import '@mordn/chat-widget/styles.css';
 
export function Assistant() {
  return <ChatWidget />;
}

That is the whole component. apiBase defaults to /api/chat; pass it explicitly if you mounted the route somewhere else.

On mount the widget calls GET ${apiBase}/bootstrap and the server returns only what the browser is allowed to see — greeting, theme, layout, features, starter prompts. The widget never sends a userId, so it cannot assert who it is or whose history it may read.

5

Send a message

Render <Assistant /> on any page, sign in, and type. The first message creates the conversation; reload the page and it is still there.

What you did not have to do

HostedSelf-hosted
Provision PostgresNot neededRequired
Run migrationsNot neededdrizzle-kit push
Create a private bucketNot neededRequired for uploads
Model credentialsYour gateway key, automatic on VercelProvider package plus provider key
Packages to install37

Note

How the model runs. When you do not pass model in code, the handler uses the model from your published agent config and hands that string to the AI SDK, which routes it through the AI Gateway. On Vercel this authenticates automatically; elsewhere, set AI_GATEWAY_API_KEY.

Inference happens in your deployment, on your gateway credentials. mordn stores the conversation and returns the config; it never proxies the model call and never bills you for tokens.

To pin a model in code instead, install the provider package and pass model — see Models & tools. Code always wins over published configuration.

Verify before shipping

  • Two signed-in users cannot load, continue, delete, or attach files to each other's conversations.
  • Signed-out requests get 401, not an empty conversation.
  • History survives a refresh.
  • MORDN_CHAT_KEY appears only in server-side code and never in a NEXT_PUBLIC_ variable.
  • If the widget and handler are on different origins, configure cors and requestCredentials deliberately.

Next steps