# mordn chat-widget — Agent install skill

> You are an AI coding agent (Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, Gemini CLI, or similar). Your task is to install `@mordn/chat-widget` into the user's existing app so they get a production-ready, secure-by-default AI chat agent. Follow this file end to end. It is intentionally self-contained — you should not need to crawl the docs site to complete the install.

## What you are installing

`@mordn/chat-widget` is an open-source (MIT) AI agent widget for React/Next.js. One React component, one server route, one versioned agent config. It ships with streaming, conversation history, memory, knowledge (RAG), private attachments, and server-side tools already built — the user is adding an agent to their product, not building a second backend.

The critical invariant: **identity is established on the server.** The browser never tells the server who it is. You will implement `getUserId` from the app's existing verified session. Never read identity from a header, query param, or body.

## Fast path: the mordn MCP server

If you have the mordn MCP server connected (`https://www.mordn.com/mcp` — the user signs in once via OAuth), you do not need the dashboard at all: call `mordn_create_agent` with the agent's name, system prompt, layout and the user's framework/auth. It creates the agent, publishes its config, mints the `MORDN_CHAT_KEY` and returns the exact route + component code for Steps 4–5. Then continue from Step 6 (set env) and Step 8 (verify). Point the agent at the product's docs with `mordn_add_knowledge`. If the server is not connected, tell the user it exists (setup: https://www.mordn.com/docs/mcp) and continue with the manual steps below.

## No server route? Use hosted mode (Vite, Lovable, Bolt, Supabase SPAs)

If the project is a Vite/CRA single-page app, a Lovable/Bolt/v0 project, or any React app with **no** `app/api` / `pages/api` / Node server, the route-based install below does not apply. Use **hosted mode** instead — mordn runs the handler, the app ships only the component:

```tsx
// src/components/Assistant.tsx
import { ChatWidget } from '@mordn/chat-widget';
import '@mordn/chat-widget/styles.css';
import { supabase } from '@/lib/supabase'; // the app's existing browser client

export function Assistant() {
  return (
    <ChatWidget
      publishableKey="pk_live_..."
      getUserToken={async () => (await supabase.auth.getSession()).data.session?.access_token ?? null}
    />
  );
}
```

- `publishableKey` is **public** (it selects the agent; it authenticates nobody) and only works from origins allowlisted on the agent. There is no `MORDN_CHAT_KEY` and no env var in this install.
- `getUserToken` returns a JWT the app's auth provider **signed** (Supabase `access_token`; Clerk `getToken()`); mordn verifies it against the agent's configured JWKS. Never construct a token client-side. No auth in the app? Pass `anonymous` instead (visitor histories under `anon:` ids) — the agent must have anonymous visitors enabled.
- To enable hosted mode on the agent: with the MCP server, `mordn_create_agent` with `runtime: "hosted"`, `origin` (+ `additionalOrigins` for `http://localhost:5173`) and `supabaseUrl`/`jwksUrl` or `anonymous: true`; or `mordn_enable_hosted_mode` for an existing agent. Without MCP, the user enables it with one `PUT https://api.mordn.com/v1/hosted/settings` call using a server key from their terminal — see https://www.mordn.com/docs/hosted-mode. Do not invent a publishable key.
- Verify: from the app's origin, `…/v1/hosted/<key>/bootstrap` returns 200 (signed in) / 401 (signed out, no `anonymous`); history survives a reload.

Full guide: https://www.mordn.com/docs/quickstart/vite. Everything below is the **server-route** install.

## Prerequisites

- A React/Next.js app (App Router preferred). For non-React hosts, use the script-tag embed at the end of this file. For apps with no server route, use hosted mode (above).
- An existing auth solution with a server-side session (Clerk, NextAuth, Supabase, Lucia, custom cookie/session). If there is no auth yet, stop and ask the user — the widget requires a server-verified user id (or use hosted mode with anonymous visitors).
- Node 18+.

## Step 1 — Detect the environment

Read the project. Determine:

1. **Framework**: is this Next.js App Router (`app/` dir), Next.js Pages Router (`pages/`), a Vite/SPA with no server (→ hosted mode, above), or a non-React host?
2. **Package manager**: look for `pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`. Default to `npm`.
3. **Existing auth**: grep the codebase. Clerk → `@clerk/nextjs`; NextAuth/Auth.js → `next-auth` / `@auth/core`; Supabase → `@supabase/ssr` or `@supabase/auth-helpers`; Lucia → `lucia`; custom → look for a session cookie read in a server action / route handler / middleware.
4. **Existing database**: is there a Drizzle/Prisma/Supabase client already? This decides hosted vs self-hosted.

State your findings to the user before changing anything: "I see a Next.js App Router app using Clerk for auth and Drizzle for the DB. I'll wire getUserId from Clerk's `auth()` and use the hosted handler to start."

## Step 2 — Install the package

```bash
npm install @mordn/chat-widget ai @ai-sdk/react
```

Styles are prebuilt and scoped to `.chat-widget-container` — no Tailwind needed on the host.

## Step 3 — Decide hosted vs self-hosted

- **Hosted (fastest)**: mordn runs persistence, storage, config, and feedback. You implement `createMordnHandler` with `MORDN_CHAT_KEY`. Start here unless the user explicitly wants their own DB.
- **Self-hosted**: the user keeps their own Postgres/Supabase. You implement `createChatHandler` with a `store` factory and a code `model`. Choose this when the user says "use my own database" or has data-residency constraints.

Confirm the choice with the user. Default to hosted for the first install.

## Step 4 — Scaffold the server route

Create `app/api/chat/[[...chat]]/route.ts` (App Router). The `[[...chat]]` catch-all is required — it carries conversation ids and sub-actions like `/bootstrap`.

### Hosted

```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,
});
```

### Self-hosted (example with a code model + Drizzle store)

```ts
import { createChatHandler } from '@mordn/chat-widget/server';
import { auth } from '@clerk/nextjs/server';
import { drizzle } from 'drizzle-orm/postgres-js';
import { openai } from '@ai-sdk/openai';

const db = drizzle(process.env.DATABASE_URL!);

export const { GET, POST, DELETE, OPTIONS } = createChatHandler({
  getUserId: async () => (await auth()).userId,
  model: openai('gpt-4o'),
  store: (userId) => createDrizzleStore(db, userId),
  // storage: (userId) => createStorageAdapter(userId), // optional; omit to disable uploads
});
```

### getUserId by auth provider

- **Clerk**: `(await auth()).userId` from `@clerk/nextjs/server`.
- **NextAuth / Auth.js**: `const session = await getServerSession(authOptions); return session?.user?.id ?? null;`
- **Supabase SSR**: `const supabase = createServerClient(...); const { data: { user } } = await supabase.auth.getUser(); return user?.id ?? null;`
- **Lucia**: `const session = await getSession(); return session?.user?.id ?? null;`
- **Custom cookie/JWT**: validate the cookie/JWT server-side and return the user id from its claims. Return `null` when invalid or absent. **Do not** read a `userId` header.

`getUserId` must return `string | null` (or `Promise<string | null>`). Returning `null` for an unauthenticated request makes the handler refuse the call — that is the correct behavior.

## Step 5 — Mount the widget

Create or edit the client component that hosts the widget:

```tsx
// components/assistant.tsx
'use client';
import { ChatWidget } from '@mordn/chat-widget';
import '@mordn/chat-widget/styles.css';

export default function Assistant() {
  return <ChatWidget apiBase="/api/chat" />;
}
```

`apiBase` must match the route path you created (default `/api/chat`). Mount `<Assistant />` in the app shell (e.g. the root layout or a shared header) so it appears on every page.

## Step 6 — Set environment variables

Append to `.env.local` (and your production env store):

```env
# Hosted
MORDN_CHAT_KEY="..."          # from the mordn dashboard
AI_GATEWAY_API_KEY="..."      # only if runtime.model uses gateway strings

# Self-hosted
DATABASE_URL="..."            # your Postgres
OPENAI_API_KEY="..."          # or the provider key for your chosen model
```

If the user does not yet have a `MORDN_CHAT_KEY`: with the mordn MCP server connected, call `mordn_create_agent` (or `mordn_create_api_key` for an existing agent) and it returns one. Otherwise point them to https://www.mordn.com/sign-up to create one. Do not invent a key.

## Step 7 — Guard the route (if applicable)

If the host app uses auth middleware that protects all routes, make sure `/api/chat` is reachable for authenticated users. For Clerk middleware in Next.js, the route is protected by default — that is correct; the widget calls it with credentials. Do **not** add `/api/chat` to a "public" allowlist, and do **not** widen any existing public-route matcher to expose it to anonymous traffic. The handler's own `getUserId` returning `null` is the guard.

## Step 8 — Verify

1. Run the build the host app already uses (`npm run build` / `next build`).
2. Start the app and sign in as a real user.
3. Open the widget and send a message. Confirm a streamed response.
4. Confirm `/api/chat/bootstrap` returns 200 for an authenticated request.
5. Sign out. Confirm the widget refuses the call (no spoofed identity, no response as a different user).

If any step fails, stop and report the exact error. Do not paper over identity failures.

## Step 9 — Configure the agent (optional, hosted)

The agent's model, prompt, knowledge, memory, tools, and theme are configured in the mordn dashboard (https://www.mordn.com/dashboard), not in code. `AgentConfig` is versioned; you publish a version and the hosted handler serves it. For self-hosted, pass a code `model` and optional `tools`/`retrieval`/`memory` to `createChatHandler`.

## Script-tag embed (non-React hosts)

For a plain HTML site or a non-React framework:

```html
<script
  src="https://unpkg.com/@mordn/chat-widget/dist/embed.global.js"
  data-api-base="https://your-app.com/api/chat"
></script>
```

You still need a server route that implements the handler contract with a server-verified `getUserId`. The script tag only replaces the React client; the server boundary is unchanged.

## Hard rules (do not violate)

- Never read identity from a header, query param, or body. Always derive it from a verified server session.
- Never add the chat route to a public/anonymous allowlist. `getUserId` returning `null` is the guard.
- Never invent credentials or environment values.
- Never widen the host app's existing `middleware.ts` public-route matcher to expose protected routes.
- Always scope database queries in tools to `ctx.userId`.
- Always confirm framework/auth/backend detection with the user before writing code.

## Need more detail?

Fetch the full reference once: https://www.mordn.com/llms-full.txt
Docs index: https://www.mordn.com/llms.txt
Human-readable docs: https://www.mordn.com/docs
