Documents
custom-backend
custom-backend
Type
External
Status
Published
Created
Mar 17, 2026
Updated
Mar 17, 2026

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#

The simplest approach — keep thread management local, but send messages to your backend for inference.

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 }] };
    }
  },
};
import { useLocalRuntime, AssistantRuntimeProvider } from "@assistant-ui/react-ink";
import { myChatAdapter } from "./adapters/my-chat-adapter.js";

export function App() {
  const runtime = useLocalRuntime(myChatAdapter);
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your chat UI */}
    </AssistantRuntimeProvider>
  );
}

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#

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#

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, unstable_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: unstable_messages }),
      });
      const { title } = await res.json();
      controller.appendText(title);
    });
  },
};

Compose the runtime#

import {
  useLocalRuntime,
  useRemoteThreadListRuntime,
  AssistantRuntimeProvider,
} 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 (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your chat UI */}
    </AssistantRuntimeProvider>
  );
}

Adapter methods#

MethodDescription
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, unstable_messages)Return an AssistantStream with the generated title

Which option to choose?#

Option 1: ChatModelAdapterOption 2: RemoteThreadListAdapter
Thread storageIn-memory (process lifetime)Your backend
Message storageIn-memoryOn-device (can add history adapter for server-side)
Cross-session persistenceNoYes
Setup complexityMinimalModerate
Best forCLI tools, demos, prototypesProduction apps with persistence