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

```ts
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](/docs/backend).

The backend must allow the origin of your app. See [Allow your app origin](/docs/backend#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](/docs/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).

```ts
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.

```ts
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](/docs/durable-chat#updates-closure-and-recovery) 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.
