# Custom Backend URL: /docs/ink/custom-backend Connect your terminal app to your own backend API. By default, `useLocalRuntime` manages threads and messages in memory. You can connect to your own backend in two ways depending on your needs. Option 1: ChatModelAdapter only \[#option-1-chatmodeladapter-only] The simplest approach — keep thread management local, but send messages to your backend for inference. ```tsx title="adapters/my-chat-adapter.ts" import type { ChatModelAdapter } from "@assistant-ui/react-ink"; export const myChatAdapter: ChatModelAdapter = { async *run({ messages, abortSignal }) { const response = await fetch("https://my-api.com/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), signal: abortSignal, }); const reader = response.body?.getReader(); if (!reader) throw new Error("No response body"); const decoder = new TextDecoder(); let fullText = ""; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); fullText += chunk; yield { content: [{ type: "text", text: fullText }] }; } }, }; ``` ```tsx title="app.tsx" import { useLocalRuntime, AssistantProvider } from "@assistant-ui/react-ink"; import { myChatAdapter } from "./adapters/my-chat-adapter.js"; export function App() { const runtime = useLocalRuntime(myChatAdapter); return ( {/* your chat UI */} ); } ``` This gives you: * Streaming chat responses from your API * In-memory thread list (lost on process exit) * Multi-thread support Option 2: Full backend thread management \[#option-2-full-backend-thread-management] When you want your backend to own thread state (e.g. for persistence across sessions, team sharing, or server-side history), implement a `RemoteThreadListAdapter`. Implement the adapter \[#implement-the-adapter] ```tsx title="adapters/my-thread-list-adapter.ts" import type { RemoteThreadListAdapter } from "@assistant-ui/react-ink"; import { createAssistantStream } from "assistant-stream"; const API_BASE = "https://my-api.com"; export const myThreadListAdapter: RemoteThreadListAdapter = { async list() { const res = await fetch(`${API_BASE}/threads`); const threads = await res.json(); return { threads: threads.map((t: any) => ({ remoteId: t.id, status: t.archived ? "archived" : "regular", title: t.title, })), }; }, async initialize(localId) { const res = await fetch(`${API_BASE}/threads`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ localId }), }); const { id } = await res.json(); return { remoteId: id, externalId: undefined }; }, async rename(remoteId, title) { await fetch(`${API_BASE}/threads/${remoteId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); }, async archive(remoteId) { await fetch(`${API_BASE}/threads/${remoteId}/archive`, { method: "POST", }); }, async unarchive(remoteId) { await fetch(`${API_BASE}/threads/${remoteId}/unarchive`, { method: "POST", }); }, async delete(remoteId) { await fetch(`${API_BASE}/threads/${remoteId}`, { method: "DELETE" }); }, async fetch(remoteId) { const res = await fetch(`${API_BASE}/threads/${remoteId}`); const t = await res.json(); return { remoteId: t.id, status: t.archived ? "archived" : "regular", title: t.title, }; }, async generateTitle(remoteId, messages) { return createAssistantStream(async (controller) => { const res = await fetch(`${API_BASE}/threads/${remoteId}/title`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), }); const { title } = await res.json(); controller.appendText(title); }); }, }; ``` Compose the runtime \[#compose-the-runtime] ```tsx title="app.tsx" import { useLocalRuntime, useRemoteThreadListRuntime, AssistantProvider, } from "@assistant-ui/react-ink"; import { myChatAdapter } from "./adapters/my-chat-adapter.js"; import { myThreadListAdapter } from "./adapters/my-thread-list-adapter.js"; function useAppRuntime() { return useRemoteThreadListRuntime({ runtimeHook: () => useLocalRuntime(myChatAdapter), adapter: myThreadListAdapter, }); } export function App() { const runtime = useAppRuntime(); return ( {/* your chat UI */} ); } ``` Adapter methods \[#adapter-methods] | Method | Description | | ----------------------------------- | ---------------------------------------------------- | | `list()` | Return all threads on mount | | `initialize(localId)` | Create a thread server-side, return `{ remoteId }` | | `rename(remoteId, title)` | Persist title changes | | `archive(remoteId)` | Mark thread as archived | | `unarchive(remoteId)` | Restore archived thread | | `delete(remoteId)` | Permanently remove thread | | `fetch(remoteId)` | Fetch single thread metadata | | `generateTitle(remoteId, messages)` | Return an `AssistantStream` with the generated title | Which option to choose? \[#which-option-to-choose] | | Option 1: ChatModelAdapter | Option 2: RemoteThreadListAdapter | | ----------------------------- | ---------------------------- | --------------------------------------------------- | | **Thread storage** | In-memory (process lifetime) | Your backend | | **Message storage** | In-memory | On-device (can add history adapter for server-side) | | **Cross-session persistence** | No | Yes | | **Setup complexity** | Minimal | Moderate | | **Best for** | CLI tools, demos, prototypes | Production apps with persistence |