Runtimes

A runtime sends messages to a model, calls tools and keeps the conversation history. The chat UI shows the conversation of one runtime. All runtimes use the same chat UI.

Remote runtime (Shappy Backend)

Use RemoteShappyChatRuntime when Shappy Backend runs the conversation. The browser shows the conversation and sends messages. The backend does the work.

import { RemoteShappyChatRuntime, mountShappyChat } from '@shappychat/chat';

const runtime = await RemoteShappyChatRuntime.connect({
  url: backendUrl,
  token: appCredential,
  conversationId,
});
const chat = mountShappyChat(document.querySelector('#chat')!, { runtime });

// Later:
chat.unmount();   // removes the UI; the backend continues a running reply
runtime.close();  // stops the updates of this client

To create a conversation, send POST /api/conversations with an empty JSON object. The response contains a conversationId. To list conversations, send GET /api/conversations. Both requests need the app credential. See Shappy Backend.

The backend must allow the origin of your app. See Allow your app origin.

While the chat is mounted, the remote runtime receives each change as a server-sent event. If the connection fails, it connects again and gets only the newer state. It does not send a message again automatically. With an older backend that has no event stream, it reads the state every 500 ms.

Durable Chat (celld)

Use CelldShappyChatRuntime in a trusted backend to connect the shared chat UI to a configured celld host. The cell keeps conversation history. The adapter accepts text and App MCP tools. See Durable Chat for source SDK setup, cell binding, browser integration and current limits.

Browser-local runtime

Use createShappyChatRuntime when the browser can reach your MCP server and your model endpoint directly. The history stays in the browser (IndexedDB).

import {
  createShappyChatRuntime,
  discoverOpenAiModels,
  McpConnections,
  mountShappyChat,
} from '@shappychat/chat';

const connections = new McpConnections();
await connections.add({
  id: 'my-app',
  url: config.mcpUrl,
  headers: { Authorization: `Bearer ${config.mcpKey}` },
});

const models = await discoverOpenAiModels(config.modelUrl);
const runtime = await createShappyChatRuntime({
  scope: { accountId: user.id, workspaceId: workspace.id },
  conversationId: savedConversationId, // omit for a new chat
  connections,
  model: models[0],
  getApiKey: () => config.modelKey || 'local',
});
mountShappyChat(container, { runtime });

// Save this value to open the same conversation later.
const id = runtime.getSnapshot().conversationId;
  • The MCP server and the model endpoint must accept requests from your origin (CORS).
  • The work stops when the user closes the page.

Node runtime

Use the Node entry point for a desktop app, for example in an Electron main process. The history is in a SQLite file in a folder that you choose.

import { addStdioConnection, createNodeShappyChatRuntime } from '@shappychat/chat/node';

const owned = await createNodeShappyChatRuntime({
  storageDirectory: path.join(app.getPath('userData'), 'shappychat'),
  scope: { accountId: account.id, workspaceId: workspace.id },
  model: selectedModel,
  getApiKey: () => providerKey,
});
await addStdioConnection(owned.runtime.connections, appMcpConfiguration);

// When the app quits:
await owned.close();
  • Only one process can own a SQLite database. Other windows connect to that process.
  • Give the renderer a narrow, authenticated connection to the runtime. Do not give the chat renderer unrestricted IPC or process access.

Scope

Each conversation belongs to a scope: an account ID and a workspace ID. Other scopes cannot see the conversations of a scope.

Warning: Set the scope from trusted configuration, for example from the signed-in session on your server. Do not take the scope from a chat message or from the request body.

Local runtime restart

These rules apply to the browser-local runtime and the Node runtime. Shappy Backend runs each conversation in a celld cell. The cell follows the recovery policy of celld.

  • The conversation history stays.
  • A tool call that was running is shown as an error with an unknown result. The runtime does not run it again.
  • A reply that was running does not continue.

If a tool call changes data in your app, use the idempotency rules of your app. Then a repeated request does not change data twice.

↑ ↓ select · Enter open · Escape close