
Building Chat Interfaces
- 14 installs
- 1 repo stars
- Updated January 27, 2026
- bilalmk/todo_correct
building-chat-interfaces is a skill for building AI chat interfaces with ChatKitServer and useChatKit, adding custom backends, JWT auth, and context injection.
About
building-chat-interfaces is a skill for building AI chat interfaces with custom backends, authentication, and context injection. On the frontend it uses useChatKit with a custom fetch to inject auth headers and page context; on the backend it uses ChatKitServer with a custom agent, database persistence, and JWT/JWKS auth. A developer uses it to integrate a chat UI with AI agents, add auth, or inject user and page context. It is not meant for simple chatbots without persistence or custom agent integration.
- Builds AI chat interfaces with ChatKitServer and useChatKit
- Injects auth headers and page/user context via a custom fetch interceptor
- Covers DB persistence and JWT/JWKS authentication for the chat backend
Building Chat Interfaces by the numbers
- 14 all-time installs (skills.sh)
- Ranked #11,296 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
building-chat-interfaces capabilities & compatibility
Requires an LLM/agent API key for the backend agent
- Capabilities
- chat ui building · agent integration · context injection
- Works with
- openai
- Use cases
- frontend · api development · orchestration
- Pricing
- Bring your own API key
What building-chat-interfaces says it does
Build AI chat interfaces with custom backends, authentication, and context injection.
Covers ChatKitServer, useChatKit, and MCP auth patterns.
// Custom fetch to inject auth and context
npx skills add https://github.com/bilalmk/todo_correct --skill building-chat-interfacesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14 |
|---|---|
| repo stars | ★ 1 |
| Last updated | January 27, 2026 |
| Repository | bilalmk/todo_correct ↗ |
What it does
Build an AI chat interface with ChatKit, custom backend, JWT auth, and page/user context injection.
Who is it for?
Integrating a chat UI with a custom AI agent backend and auth
Skip if: Simple chatbots without persistence or custom agent integration
When should I use this skill?
Integrating chat UI with AI agents, adding auth to chat, or injecting user/page context
What you get
A chat interface with a custom agent backend, DB persistence, JWT auth, and injected context.
- ChatKit server with custom agent
- React chat UI with context injection
- JWT-authenticated chat backend
By the numbers
- Three backend patterns: custom-agent server, DB persistence, JWT/JWKS auth
Files
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 event2. 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
httpOnly Cookie Proxy
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
Related Skills (Tiered System)
- 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
- references/chatkit-integration-patterns.md - Complete patterns with evidence
- references/nextjs-httponly-proxy.md - Next.js cookie proxy patterns
ChatKit Integration Patterns
Complete patterns for ChatKit integration with evidence references.
Backend Principles
1. Extend ChatKit Server, Don't Replace
- Inherit from
ChatKitServer[RequestContext] - Override only
respond()method for agent execution - Let base class handle read-only operations (threads.list, items.list)
- Rationale: ChatKit handles protocol, you handle agent logic
2. Context Injection in Prompt
- Include conversation history as string in system prompt
- Include user context (name, profile) in system prompt
- Include page context (current page) in system prompt
- Rationale: Agent SDK receives single prompt, history must be in prompt
3. User Isolation via RequestContext
- All operations scoped by
user_idinRequestContext - Store operations filter by
user_idautomatically - Never expose data across users
- Rationale: Multi-tenant safety, data privacy
4. Connection Pool Warmup
- Pre-warm database connections on startup
- Avoids 7+ second first-request delay
- Test connections before use (
pool_pre_ping=True)
Frontend Principles
1. Custom Fetch Interceptor
- Provide custom
fetchfunction touseChatKitconfig - Add authentication headers (
X-User-ID) - Add metadata (userInfo, pageContext) to request body
2. Build-Time Configuration
- Read env vars in build config (docusaurus.config.ts)
- Add to
customFieldsfor client-side access - Don't use
process.envin browser code
3. Authentication Gate
- Require login before allowing chat access
- Show login prompt if not authenticated
- Redirect to OAuth flow
Text Selection "Ask" Feature
Allow users to ask questions about selected content:
useEffect(() => {
const handleSelection = () => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
setSelectedText('');
return;
}
const selectedText = selection.toString().trim();
if (selectedText.length > 0) {
setSelectedText(selectedText);
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
setSelectionPosition({
x: rect.left + rect.width / 2,
y: rect.top - 10,
});
}
};
document.addEventListener('selectionchange', handleSelection);
document.addEventListener('mouseup', handleSelection);
return () => {
document.removeEventListener('selectionchange', handleSelection);
document.removeEventListener('mouseup', handleSelection);
};
}, []);
const handleAskSelectedText = useCallback(async () => {
const pageContext = getPageContext();
const messageText = `Can you explain this from "${pageContext.title}":\n\n"${selectedText}"`;
if (!isOpen) {
setIsOpen(true);
await new Promise(resolve => setTimeout(resolve, 300));
}
await sendUserMessage({
text: messageText,
newThread: false,
});
window.getSelection()?.removeAllRanges();
setSelectedText('');
}, [selectedText, isOpen, sendUserMessage, getPageContext]);Evidence: robolearn-interface/src/components/ChatKitWidget/index.tsx:153-187
Separate ChatKit Store Configuration
When ChatKit needs its own database schema/connection:
# config.py - Ignore ChatKit env vars in main Settings
class Settings(BaseSettings):
model_config = SettingsConfigDict(extra="ignore")
@property
def chat_enabled(self) -> bool:
return os.getenv("TASKFLOW_CHATKIT_DATABASE_URL") is not None
# chatkit_store/config.py - Separate config
class StoreConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="TASKFLOW_CHATKIT_")
database_url: str
schema_name: str = "taskflow_chat"Tier Boundaries
This Skill Covers (Tier 1: Foundation)
- ChatKitServer setup with
respond()method useChatKitbasic configuration- Custom fetch interceptor for authentication
- Context injection (user info, page context)
- Script loading detection
- httpOnly cookie proxy (Next.js)
- Database persistence setup
- MCP tool authentication via prompt
Use streaming-llm-responses For (Tier 2: Real-time)
onResponseStart/onResponseEndhandlersonEffectfor fire-and-forget client updatesProgressUpdateEventfor loading states- Thread lifecycle events
- Thread title generation
Use building-chat-widgets For (Tier 3: Interactive)
- Widget templates (.widget files)
widgets.onActionhandleraction()method in ChatKitServersendCustomAction()for widget updates- Entity tagging (@mentions)
- Composer tools (mode selection)
Evidence Sources
Patterns derived from:
rag-agent/chatkit_server.pyrobolearn-interface/src/components/ChatKitWidget/web-dashboard/src/app/api/chatkit/route.tsweb-dashboard/src/components/chat/ChatKitWidget.tsx
Next.js httpOnly Cookie Proxy Patterns
Why Proxy is Needed
httpOnly cookies are a security feature - they CANNOT be read by JavaScript. This protects against XSS attacks stealing tokens.
When your auth system stores JWT tokens in httpOnly cookies (like Auth0, Better Auth, etc.), the frontend cannot:
- Read the token to add to headers
- Check if user is authenticated (directly)
- Forward tokens to API calls
Solution: Server-Side API Route Proxy
Create a Next.js API route that: 1. Reads httpOnly cookies (server-side only) 2. Adds Authorization header 3. Proxies request to backend 4. Streams SSE responses back
Complete Proxy Implementation
// app/api/chatkit/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
export async function POST(request: NextRequest) {
const cookieStore = await cookies();
// Read httpOnly cookie (only accessible server-side)
const idToken = cookieStore.get("taskflow_id_token")?.value;
if (!idToken) {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}
// Build target URL - note: ChatKit endpoint is at /chatkit, NOT /api/chatkit
const url = new URL("/chatkit", API_BASE);
try {
const body = await request.text();
// Forward request with Authorization header
const response = await fetch(url.toString(), {
method: "POST",
headers: {
Authorization: `Bearer ${idToken}`,
"Content-Type": "application/json",
// Forward custom headers
"X-User-ID": request.headers.get("X-User-ID") || "",
"X-Page-URL": request.headers.get("X-Page-URL") || "",
},
body: body || undefined,
});
// Handle SSE streaming responses
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",
"Connection": "keep-alive",
},
});
}
// Return JSON for non-streaming responses
const data = await response.json().catch(() => null);
return NextResponse.json(data, { status: response.status });
} catch (error) {
console.error("[ChatKit Proxy] Error:", error);
return NextResponse.json({ error: "ChatKit proxy request failed" }, { status: 500 });
}
}Frontend Usage with Proxy
const chatkitProxyUrl = "/api/chatkit"; // Proxy handles auth
const { control, sendUserMessage } = useChatKit({
api: {
url: chatkitProxyUrl,
domainKey: domainKey,
// Custom fetch - auth handled by proxy, inject context
fetch: async (input: RequestInfo | URL, options?: RequestInit) => {
const url = typeof input === 'string' ? input : input.toString();
// Client-side auth check (proxy will verify token)
if (!isAuthenticated) {
throw new Error('User must be logged in');
}
const userId = user.sub;
const pageContext = getPageContext();
// Inject metadata into request body
let modifiedOptions = { ...options } as RequestInit;
if (modifiedOptions.body && typeof modifiedOptions.body === 'string') {
try {
const parsed = JSON.parse(modifiedOptions.body);
if (parsed.params?.input) {
parsed.params.input.metadata = {
...parsed.params.input.metadata,
userId,
userInfo: { id: userId, name: user.name },
pageContext,
};
modifiedOptions.body = JSON.stringify(parsed);
}
} catch { /* Ignore non-JSON */ }
}
return fetch(url, {
...modifiedOptions,
credentials: 'include', // Include cookies for proxy auth
headers: {
...modifiedOptions.headers,
'X-User-ID': userId,
'X-Page-URL': pageContext?.url || '',
'Content-Type': 'application/json',
},
});
},
},
});Script Loading for Web Components
ChatKit uses web components that must be defined before React renders them:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
{/* MUST be in <head> with beforeInteractive for web components */}
<Script
src="https://cdn.platform.openai.com/deployments/chatkit/chatkit.js"
strategy="beforeInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}Common Issues
| Issue | Symptom | Fix |
|---|---|---|
| Wrong backend endpoint | 404 errors | Route to /chatkit not /api/chatkit |
| Script loading too late | "ChatKit web component unavailable" | Use beforeInteractive in <head> |
| Cookies not sent | Auth fails silently | Add credentials: 'include' to fetch |
| SSE not streaming | Response arrives all at once | Return Response(body) not NextResponse.json() |
#!/usr/bin/env python3
"""Verify building-chat-interfaces skill has required references."""
import os
import sys
def main():
skill_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
refs_dir = os.path.join(skill_dir, "references")
required = ["chatkit-integration-patterns.md", "nextjs-httponly-proxy.md"]
missing = [r for r in required if not os.path.isfile(os.path.join(refs_dir, r))]
if not missing:
print("✓ building-chat-interfaces skill ready")
sys.exit(0)
else:
print(f"✗ Missing: {', '.join(missing)}")
sys.exit(1)
if __name__ == "__main__":
main()
Related skills
FAQ
How is auth and context added to requests?
A custom fetch interceptor in useChatKit injects auth headers and page/user context into the request body and headers.
When should I not use this skill?
Not when building simple chatbots without persistence or custom agent integration.