Skip to content

Bring your own database

Everything in the Quickstart applies here — same widget, same handler, same identity boundary. The difference is where conversation rows and uploaded files live: your Postgres and your bucket instead of ours.

Take this path when data residency, an existing schema, or a hard no-third-party rule makes hosted persistence a non-starter. It costs you a database, a migration step, and a model-provider key.

Note

Prerequisites: everything in the Quickstart, plus a Postgres database, and — if you want file uploads — a Supabase project.

1

Install

code
npm install @mordn/chat-widget ai @ai-sdk/react \
  drizzle-orm postgres @supabase/supabase-js @ai-sdk/anthropic
npm install -D drizzle-kit

Omit @supabase/supabase-js if you do not need uploads. Replace @ai-sdk/anthropic with the provider package you use.

2

Scaffold the backend

code
npx @mordn/chat-widget

The CLI scaffolds this path specifically. It creates four files:

FilePurpose
app/api/chat/[[...chat]]/route.tsCatch-all route for chat, history, uploads, memory, and feedback
lib/chat-auth.tsThe getChatUserId stub you must implement
drizzle.config.tsPoints Drizzle Kit at the current chat schema
.env.exampleRequired environment-variable template
3

Implement the identity boundary

code
lib/chat-auth.ts
import { auth } from '@clerk/nextjs/server';
 
export async function getChatUserId() {
  const { userId } = await auth();
  return userId;
}

Warning

Never derive this id from X-User-Id, query parameters, or the request body. Those values are controlled by the browser. Return null when unauthenticated; the handler responds 401 before touching user data.

4

Configure persistence, storage, and the model

code
.env.local
DATABASE_URL="postgres://..."
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="..."
code
app/api/chat/[[...chat]]/route.ts
import { anthropic } from '@ai-sdk/anthropic';
import { createChatHandler } from '@mordn/chat-widget/server';
import { createDrizzleChatStore } from '@mordn/chat-widget/server/drizzle';
import { createSupabaseStorage } from '@mordn/chat-widget/server/supabase';
import { getChatUserId } from '@/lib/chat-auth';
 
export const { GET, POST, DELETE, OPTIONS } = createChatHandler({
  getUserId: getChatUserId,
  model: anthropic('claude-sonnet-4-5'),
  store: createDrizzleChatStore(),
  storage: createSupabaseStorage(),
});

A model is required unless getHostedConfig supplies one. A store is currently required. Omit storage to disable uploads. Export OPTIONS when you configure cross-origin CORS.

5

Create the tables and private bucket

code
npx drizzle-kit push

This creates chat_conversations and chat_messages. If you keep createSupabaseStorage(), create a private Supabase Storage bucket named chat-attachments. Never make it public.

6

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 apiBase="/api/chat" />;
}

To set appearance in code rather than publishing it from the dashboard, pass config — the same canonical shape the bootstrap returns:

code
components/assistant.tsx
<ChatWidget
  apiBase="/api/chat"
  config={{
    schemaVersion: 1,
    runtime: { model: 'anthropic/claude-sonnet-4-5' },
    client: {
      greeting: 'How can I help?',
      theme: {
        backgroundColor: '#ffffff',
        textColor: '#262626',
        primaryColor: '#171717',
      },
      features: { fileUpload: true },
      display: { layout: 'popup', size: 'default' },
      starterPrompts: [{ title: 'What can you help me with?' }],
    },
  }}
/>

Mixing the two

The axes are independent. You can keep conversations in your own Postgres while still using hosted configuration, knowledge, and feedback — useful when the data-residency rule is about message content, not about which model you picked.

code
import { createChatHandler } from '@mordn/chat-widget/server';
import { createDrizzleChatStore } from '@mordn/chat-widget/server/drizzle';
import {
  createHostedConfig,
  createHostedFeedback,
} from '@mordn/chat-widget/server/hosted';
 
const hosted = { apiKey: process.env.MORDN_CHAT_KEY! };
 
export const { GET, POST, DELETE } = createChatHandler({
  getUserId: getChatUserId,
  store: createDrizzleChatStore(),
  getHostedConfig: createHostedConfig(hosted),
  onFeedback: createHostedFeedback(hosted),
});

See Choosing a backend for every combination.

Verify before shipping

  • Two signed-in users cannot load, continue, delete, or attach files to each other's conversations.
  • The attachments bucket is private.
  • The route works after refresh and history reload.
  • If the widget and handler are on different origins, configure cors and requestCredentials deliberately.

Next steps