# 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 |