building-chat-interfaces

mjunaidca's avatarfrom mjunaidca

Build AI chat interfaces with custom backends, authentication, and context injection.Use when integrating chat UI with AI agents, adding auth to chat, injecting user/page context,or implementing httpOnly cookie proxies. Covers ChatKitServer, useChatKit, and MCP auth patterns.NOT when building simple chatbots without persistence or custom agent integration.

⭐1stars🔀2forks📁View on GitHub🕐Updated Dec 22, 2025

When & Why to Use This Skill

This Claude skill provides a comprehensive framework for building production-grade AI chat interfaces, focusing on secure backend integration, multi-layered authentication, and intelligent context injection. It bridges the gap between frontend UI components and complex AI agent logic, enabling developers to create context-aware, secure, and persistent chat experiences using modern stacks like React, Python, and Next.js.

Use Cases

  • Enterprise AI Integration: Building secure, authenticated chat interfaces that connect internal AI agents with corporate user databases and identity providers.
  • Context-Aware Assistance: Automatically injecting real-time website metadata, page content, and user session data into the AI's prompt to provide hyper-relevant, environment-aware responses.
  • Secure Token Handling: Implementing server-side proxies for httpOnly cookies in frameworks like Next.js to protect sensitive authentication tokens while maintaining seamless AI communication.
  • Persistent Chat Systems: Developing custom backend storage and retrieval patterns to maintain long-term conversation history and user preferences across multiple sessions.
namebuilding-chat-interfaces
description|

Building Chat Interfaces

Build production-grade AI chat interfaces with custom backend integration.

Quick Start

# Backend (Python)
uv add chatkit-sdk agents httpx

# Frontend (React)
npm install @openai/chatkit-react

Core Architecture

Frontend (React)                    Backend (Python)
┌─────────────────┐                ┌─────────────────┐
│  useChatKit()   │───HTTP/SSE───>│  ChatKitServer  │
│  - custom fetch │                │  - respond()    │
│  - auth headers │                │  - store        │
│  - page context │                │  - agent        │
└─────────────────┘                └─────────────────┘

Backend Patterns

1. ChatKit Server with Custom Agent

from chatkit.server import ChatKitServer
from chatkit.agents import stream_agent_response
from agents import Agent, Runner

class CustomChatKitServer(ChatKitServer[RequestContext]):
    """Extend ChatKit server with custom agent."""

    async def respond(
        self,
        thread: ThreadMetadata,
        input_user_message: UserMessageItem | None,
        context: RequestContext,
    ) -> AsyncIterator[ThreadStreamEvent]:
        if not input_user_message:
            return

        # Load conversation history
        previous_items = await self.store.load_thread_items(
            thread.id, after=None, limit=10, order="desc", context=context
        )

        # Build history string for prompt
        history_str = "\n".join([
            f"{item.role}: {item.content}"
            for item in reversed(previous_items.data)
        ])

        # Extract context from metadata
        user_info = context.metadata.get('userInfo', {})
        page_context = context.metadata.get('pageContext', {})

        # Create agent with context in instructions
        agent = Agent(
            name="Assistant",
            tools=[your_search_tool],
            instructions=f"{history_str}\nUser: {user_info.get('name')}\n{system_prompt}",
        )

        # Run agent with streaming
        result = Runner.run_streamed(agent, input_user_message.content)
        async for event in stream_agent_response(context, result):
            yield event

2. Database Persistence

from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine

DATABASE_URL = os.getenv("DATABASE_URL").replace("postgresql://", "postgresql+asyncpg://")
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)

# Pre-warm connections on startup
async def warmup_pool():
    async with engine.begin() as conn:
        await conn.execute(text("SELECT 1"))

3. JWT/JWKS Authentication

from jose import jwt
import httpx

async def get_current_user(authorization: str = Header()):
    token = authorization.replace("Bearer ", "")
    async with httpx.AsyncClient() as client:
        jwks = (await client.get(JWKS_URL)).json()
    payload = jwt.decode(token, jwks, algorithms=["RS256"])
    return payload

Frontend Patterns

1. Custom Fetch Interceptor

const { control, sendUserMessage } = useChatKit({
  api: {
    url: `${backendUrl}/chatkit`,
    domainKey: domainKey,

    // Custom fetch to inject auth and context
    fetch: async (url: string, options: RequestInit) => {
      if (!isLoggedIn) {
        throw new Error('User must be logged in');
      }

      const pageContext = getPageContext();
      const userInfo = { id: userId, name: user.name };

      // Inject metadata into request body
      let modifiedOptions = { ...options };
      if (modifiedOptions.body && typeof modifiedOptions.body === 'string') {
        const parsed = JSON.parse(modifiedOptions.body);
        if (parsed.params?.input) {
          parsed.params.input.metadata = {
            userId, userInfo, pageContext,
            ...parsed.params.input.metadata,
          };
          modifiedOptions.body = JSON.stringify(parsed);
        }
      }

      return fetch(url, {
        ...modifiedOptions,
        headers: {
          ...modifiedOptions.headers,
          'X-User-ID': userId,
          'Content-Type': 'application/json',
        },
      });
    },
  },
});

2. Page Context Extraction

const getPageContext = useCallback(() => {
  if (typeof window === 'undefined') return null;

  const metaDescription = document.querySelector('meta[name="description"]')
    ?.getAttribute('content') || '';

  const mainContent = document.querySelector('article') ||
                     document.querySelector('main') ||
                     document.body;

  const headings = Array.from(mainContent.querySelectorAll('h1, h2, h3'))
    .slice(0, 5)
    .map(h => h.textContent?.trim())
    .filter(Boolean)
    .join(', ');

  return {
    url: window.location.href,
    title: document.title,
    path: window.location.pathname,
    description: metaDescription,
    headings: headings,
  };
}, []);

3. Script Loading Detection

const [scriptStatus, setScriptStatus] = useState<'pending' | 'ready' | 'error'>(
  isBrowser && window.customElements?.get('openai-chatkit') ? 'ready' : 'pending'
);

useEffect(() => {
  if (!isBrowser || scriptStatus !== 'pending') return;

  if (window.customElements?.get('openai-chatkit')) {
    setScriptStatus('ready');
    return;
  }

  customElements.whenDefined('openai-chatkit').then(() => {
    setScriptStatus('ready');
  });
}, []);

// Only render when ready
{isOpen && scriptStatus === 'ready' && <ChatKit control={control} />}

Next.js Integration

When auth tokens are in httpOnly cookies (can't be read by JavaScript):

// app/api/chatkit/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
  const cookieStore = await cookies();
  const idToken = cookieStore.get("auth_token")?.value;

  if (!idToken) {
    return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
  }

  const response = await fetch(`${API_BASE}/chatkit`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${idToken}`,
      "Content-Type": "application/json",
    },
    body: await request.text(),
  });

  // Handle SSE streaming
  if (response.headers.get("content-type")?.includes("text/event-stream")) {
    return new Response(response.body, {
      status: response.status,
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
      },
    });
  }

  return NextResponse.json(await response.json(), { status: response.status });
}

Script Loading Strategy

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        {/* MUST be beforeInteractive for web components */}
        <Script
          src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
          strategy="beforeInteractive"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

MCP Tool Authentication

MCP protocol doesn't forward auth headers. Pass credentials via system prompt:

SYSTEM_PROMPT = """You are Assistant.

## Authentication Context
- User ID: {user_id}
- Access Token: {access_token}

CRITICAL: When calling ANY MCP tool, include:
- user_id: "{user_id}"
- access_token: "{access_token}"
"""

# Format with credentials
instructions = SYSTEM_PROMPT.format(
    user_id=context.user_id,
    access_token=context.metadata.get("access_token", ""),
)

Common Pitfalls

Issue Symptom Fix
History not in prompt Agent doesn't remember conversation Include history as string in system prompt
Context not transmitted Agent missing user/page info Add to request metadata, extract in backend
Script not loaded Component fails to render Detect script loading, wait before rendering
Auth headers missing Backend rejects requests Use custom fetch interceptor
httpOnly cookies Can't read token from JS Create server-side API route proxy
First request slow 7+ second delay Pre-warm database connection pool

Verification

Run: python3 scripts/verify.py

Expected: ✓ building-chat-interfaces skill ready

If Verification Fails

  1. Check: references/ folder has chatkit-integration-patterns.md
  2. Stop and report if still failing
  • streaming-llm-responses - Tier 2: Response lifecycle, progress updates, client effects
  • building-chat-widgets - Tier 3: Interactive widgets, entity tagging, composer tools
  • fetching-library-docs - ChatKit docs: --library-id /openai/chatkit --topic useChatKit

References