# ShappyChat documentation > ShappyChat is one chat backend for all your apps. You run Shappy Backend on your own server and connect each app that must have a chat. You manage the models, MCP tools, access and history of all these apps in one place. --- Source: /docs/ (Markdown: /docs/overview.md) # Overview ShappyChat is one chat backend for all your apps. You run Shappy Backend on your own server and connect each app that must have a chat. Then you manage the models, tools, access and history of all these apps in one place. ![Four apps connect to one Shappy Backend on your server: an internal tool, a customer product, the Chrome extension and your own chat app. Each app has its own credential. The backend is the control plane: models, connected apps, tools and history. It runs each conversation in a celld cell and calls your model server and your MCP tools.](/docs/diagrams/architecture.svg "One backend for every app") > **Status:** Version 0.1.0 is a self-hosted release for one operator. The SDK is not on npm; your backend serves it as a package file. The Chrome extension is a zip file and is not in the Chrome Web Store. ## In one minute - **Put it in any app.** Use it in internal tools or in a product that your customers use. You can also use it in the Chrome extension or in your own chat app. Each app gets its own look, tools and credential. - **One control plane.** You manage the models, connected apps, MCP tools and history of every app in one place. When you revoke one app, the other apps continue to run. - **Your tools and your screen.** The chat calls the MCP tools that your app already has, and it can use page controls that you tag. - **Durable.** celld keeps the conversations. They stay after a restart. > **Note:** A credential is for one app. It is not for one user. In a product that your customers use, your server must decide which user can see which conversation. See [Security and data](/docs/security). ## The parts | Part | What it does | Read | | --- | --- | --- | | Shappy Backend | Serves the backend pages and API, keeps settings, checks access and serves the SDK | [Shappy Backend](/docs/backend) | | celld | Runs and stores each conversation in its own cell | [Self-host](/docs/self-host) | | Embedded chat SDK | `@shappychat/chat`: the chat UI and the runtime clients for your web app | [Embed the chat](/docs/embed) | | Chrome extension | The chat in the Chrome side panel, connected to your backend or to a local model | [Chrome extension](/docs/chrome) | | Your MCP tools | The operations of your app that the chat can call | [App tools (MCP)](/docs/mcp) | | Model server | LM Studio, Ollama or any OpenAI-compatible endpoint | [Shappy Backend](/docs/backend#configure-a-model) | ## How the parts connect The chat UI does not do the work. A **runtime** does the work. The runtime sends messages to a model, calls tools and keeps the conversation history. The UI shows the conversation of one runtime. - **The host app** is your app. It owns its data and its operations. - **App tools** are the operations of your app, available through the Model Context Protocol (MCP). The chat calls these tools. The chat does not copy your business logic. - **UI tools** let the chat use controls on the page, such as buttons and fields, that you mark for the chat. - **The runtime** can run behind Shappy Backend, in the browser, or in a Node process such as an Electron main process. ## Which runtime to use | Runtime | Conversation history | Use when | | --- | --- | --- | | Shappy Backend (remote) | celld cells behind the backend | Several apps share models, tools and one control plane. This is the main path. | | Durable Chat (celld) | Pi Durable in a celld host that you configure | Your own trusted server binds conversations to cells. See [Durable Chat](/docs/durable-chat). | | Browser-local | IndexedDB in the browser | The browser can reach your MCP server and model endpoint directly. | | Node | SQLite in a folder that you choose | You build a desktop app, for example with Electron. | See [Runtimes](/docs/runtimes) for the setup of each runtime. ## Next steps 1. Run the backend on your server. See [Self-host](/docs/self-host). 2. Add the chat to your app, or let your coding agent do it. See [Embed the chat](/docs/embed) and [Set up with your agent](/docs/agents). 3. Connect the operations of your app. See [App tools (MCP)](/docs/mcp). 4. To try ShappyChat without a server first, run the Notes demo. See [Quickstart](/docs/quickstart). --- Source: /docs/agents (Markdown: /docs/agents.md) # Set up with your agent These docs are for people and for coding agents. Give your agent a link, or copy one page into it. This page gives the links and the prompt that adds ShappyChat to an app. ## Links for agents | URL | Contents | | --- | --- | | [`/llms.txt`](/llms.txt) | An index of every page, with a one-line summary of each | | [`/llms-full.txt`](/llms-full.txt) | Every page in one Markdown file | | `/docs/.md` | One page as Markdown, for example [`/docs/self-host.md`](/docs/self-host.md) | Each page also has a **Copy as Markdown** button at the top. The HTML of each page address contains the full page text. Thus a tool that does not run JavaScript can read the page. ## The setup prompt Step 6 of the backend setup, **Add to your app**, gives a prompt. The prompt contains your backend URL, your app origin and your tools. Copy the prompt into your coding agent. This is an example: ```markdown Add the ShappyChat chat to this app. Facts - Backend: https://chat.example.com (in SHAPPYCHAT_URL) - Credential: in SHAPPYCHAT_TOKEN. Keep it on the server. - App origin: https://app.example.com - Tools: notes (4 tools) Steps 1. npm install https://chat.example.com/sdk/shappychat.tgz 2. Add a server route /shappy/* for signed-in users. Forward /api/conversations and /api/models to SHAPPYCHAT_URL with Authorization: Bearer SHAPPYCHAT_TOKEN. Stream event streams. 3. In the browser, POST /shappy/api/conversations, connect RemoteShappyChatRuntime to /shappy with the user's sign-in token, then call mountShappyChatDrawer. Import its style.css. 4. Save each user's conversation ID so the chat resumes. 5. Send "List my notes" and check that the reply lists notes. ``` ## What the agent needs from you - A running Shappy Backend. See [Self-host](/docs/self-host). - A connected app with its credential and allowed origin, from **Connected apps** or setup step 4. - `SHAPPYCHAT_URL` and `SHAPPYCHAT_TOKEN` in the environment of your app server. Never in browser code. - The sign-in check of your app for the `/shappy/*` route. The route must also make sure that each user gets access only to their own conversations. ## Rules for agents - Keep the app credential on the server. Do not put it in a client bundle, a URL or a log. - Take the conversation scope from the signed-in session, not from a chat message or a request body. - Do not invent API routes. The routes are in [Shappy Backend](/docs/backend#conversations-api) and the exports are in [API reference](/docs/api). - Check the result with a real message through the whole path, for example "List my notes". --- Source: /docs/quickstart (Markdown: /docs/quickstart.md) # Quickstart Run the included Notes demo. The demo is a small notes app with the ShappyChat in it. The chat lists and creates notes through the MCP tools of the app. ## Requirements - Node.js 22.19 or newer - npm - The ShappyChat source checkout ## Run the scripted demo The scripted demo needs no model and no API key. ```bash npm ci npm run example ``` The launcher builds the SDK and the demo. Then it opens a private pairing link in your browser. 1. In the Notes header, click **Chat**. 2. Click **List my notes**. The chat calls the `list_notes` tool of the app. 3. Click **Create a demo note**. The chat calls `add_note`. The new note shows in the app. > **Note:** The scripted demo does not use an AI model. It is labelled **Scripted demo (no LLM)**. It gives fixed answers for the two example prompts. The tool calls, the MCP transport, the chat UI and the storage are real product code. ## Use a real model You can use any server that supplies an OpenAI-compatible API, for example LM Studio. Load a chat model that supports tool calls. ```bash SHAPPYCHAT_MODEL_URL=http://127.0.0.1:1234/v1 npm run example ``` If your model server needs a key, set `SHAPPYCHAT_MODEL_API_KEY` in your environment. Do not put the key in source files. ## Addresses and files | Item | Value | | --- | --- | | Notes app | `http://127.0.0.1:8850` | | Demo runtime | Port 8851 | | Scripted model | Port 8852 | | Private pairing link | `~/.shappychat/example-notes/launch-url` | All services listen only on the local computer (loopback). To start without a browser window, use this command. Then open the private pairing link from the file. ```bash npm run example -- --no-open ``` > **Warning:** The pairing link contains a key. Do not share or publish it. ## Try the chat layouts Click **Chat** in the Notes header. Select a layout: - **Right panel** (recommended): the chat slides in from the right side. - **Bottom-right dot**: a small button opens a floating chat window. - **Bottom-centre island**: a message bar at the bottom. The conversation shows above it. - **Side by side**: the app and the chat share the window. The conversation and your unsent text stay when you change the layout. --- Source: /docs/self-host (Markdown: /docs/self-host.md) # Self-host Shappy Backend Shappy Backend runs ShappyChat on a computer or server you control. It serves the chat app, keeps conversations in celld, and connects your model server, your app tools and the Chrome extension to one place. > **Status:** This release is for one operator. A multi-user hosted service is not supported. Operator sign-in gives full control of the installation, so run it only for the person who owns the data. This guide uses the Docker Compose bundle from the release page. The bundle runs two services: `backend` (the chat app and API) and `celld` (the cell host that runs conversations). Both services use the same release version. ## Install You need: - Docker with Docker Compose v2. Run `docker compose version` to check. - A model server on this computer or another reachable host, such as LM Studio or Ollama, or any OpenAI-compatible endpoint. ![The release bundle: two image archives are loaded into Docker; compose.yml starts the backend and celld containers on your server; the backend publishes port 8849 to 127.0.0.1, optionally behind an HTTPS reverse proxy, and reaches your model server through host.docker.internal](/docs/diagrams/self-host.svg) Download every file of the release into an empty directory. A release has: | File | Contents | | --- | --- | | `compose.yml`, `.env.example` | The stack and its settings | | `shappychat-backend--linux-.tar.gz` | The backend image, for `amd64` or `arm64` | | `shappychat-celld--linux-.tar.gz` | The celld image, for `amd64` or `arm64` | | `backup.sh`, `restore.sh` | Backup and restore scripts | | `shappychat-.tgz` | The SDK. The backend also serves it at `/sdk/shappychat.tgz`. | | `shappychat-extension-.zip` | The Chrome extension | | `SHA256SUMS`, `provenance.json` | Checksums and build information | Check the files against the checksums. On macOS, use `shasum -a 256 -c SHA256SUMS`. ```bash sha256sum -c SHA256SUMS ``` Load the two image archives for this computer. Use `amd64` on x86-64. Use `arm64` on Apple silicon or ARM Linux. Docker does not download these images from a registry. ```bash docker load -i shappychat-backend--linux-.tar.gz docker load -i shappychat-celld--linux-.tar.gz ``` Make the settings file. The `.env.example` of the release sets `SHAPPYCHAT_OWNER` and `SHAPPYCHAT_VERSION` to the names of the images that you loaded. The `compose.yml` of the release uses only these images. It does not build images from the source code. ```bash cp .env.example .env ``` Start the stack: ```bash docker compose up -d docker compose ps ``` Both `backend` and `celld` should show a running state and a healthy status. If one does not, see [Troubleshooting](#troubleshooting). ## First login Create the operator sign-in link from the backend container: ```bash docker compose exec backend shappy login ``` The command prints one single-use link. The link is valid for 10 minutes. Open it in a browser on a computer that can reach `SHAPPYCHAT_PUBLIC_URL`. Without a public URL the default is `http://localhost:`, where `` is `SHAPPYCHAT_PORT`, which defaults to 8849. An operator session lasts 12 hours. A restart of the backend ends every session. After a restart, run `docker compose exec backend shappy login` again. ## Model Step 2 of the setup flow tests the model server, lists its models and saves the server and the model you choose. `SHAPPYCHAT_MODEL_URL` and `SHAPPYCHAT_MODEL_API_KEY` in `.env` are only the first default values. After you save a server in setup or in **Models**, the backend uses the saved server and model. This is also true after a restart. A container cannot reach `127.0.0.1` on your computer, because that address is the container itself. For a model server on the same computer, use `host.docker.internal`: - LM Studio: `http://host.docker.internal:1234/v1` - Ollama: `http://host.docker.internal:11434/v1` ```dotenv SHAPPYCHAT_MODEL_URL=http://host.docker.internal:1234/v1 SHAPPYCHAT_MODEL_API_KEY= ``` `SHAPPYCHAT_MODEL_API_KEY` is secret. Leave it empty for local servers that need no key. An empty value does not remove a key saved in the app. The backend encrypts the keys that you save in the app with `master.key`. ## MCP To add an app that has an HTTP MCP endpoint, open **Tools / MCP** in the backend pages. Then use step 3 of the setup flow to turn on the connections that you added. Enter the MCP URL and the scoped bearer key that the app issued for ShappyChat. Use a key that grants only the operations the chat should use. The app still checks every tool call against its own permissions. Stdio MCP apps run a command inside the backend container. The backend image contains only Node.js 22 and the backend itself. A stdio app works only when its command and every dependency are inside the image. The bundle does not include other runtimes, so prefer an HTTP MCP endpoint for apps that need them. You configure a stdio connection in a JSON file. The backend reads this file through `SHAPPYCHAT_CONFIG`. Mount the file with a compose override. Save this text as `compose.override.yml` next to `compose.yml`. Docker Compose loads it automatically: ```yaml services: backend: environment: SHAPPYCHAT_CONFIG: /config/shappychat.json volumes: - ./shappychat.json:/config/shappychat.json:ro ``` ```json { "connections": [ { "transport": "stdio", "id": "notes", "command": "node", "args": ["/opt/notes-mcp/index.js"], "inheritEnv": ["NOTES_API_TOKEN"] } ] } ``` The command path must exist inside the backend container. `inheritEnv` gives the names of the variables that the command gets from the backend environment. Put their values in `.env`. Then run `docker compose up -d` to make the backend again. A command that is not in the image needs a custom image. Build the custom image from `backend.Dockerfile`. The release files do not include `backend.Dockerfile`. ## App credential Step 4 of the setup flow issues a credential for each app or client that uses the chat. For each one, enter: - The exact origins that may use the credential, for example `https://app.example.com`. - For the Chrome extension, its origin `chrome-extension://`, where `` is the extension ID in your browser. - The MCP connections that the client may use. The backend shows the credential one time only. Copy it when the backend shows it. ShappyChat keeps only a hash of the credential, so it cannot show it again. If you lose the credential, issue a new one and revoke the old one. Keep the credential on the app's server. Do not place it in browser code, public repositories or client bundles. Revoke it under **Connected apps** to deny access and cancel the turns that it started. Step 6 of the setup flow gives the agent instructions and the code for the app. The backend supplies the SDK at `/sdk/shappychat.tgz`, for example `https://chat.example.com/sdk/shappychat.tgz`. ## HTTPS reverse proxy Users on other computers need an HTTPS address. Put a reverse proxy in front of the backend and keep the backend on loopback. In `.env`: ```dotenv SHAPPYCHAT_PUBLIC_URL=https://chat.example.com SHAPPYCHAT_BIND=127.0.0.1 SHAPPYCHAT_PORT=8849 ``` Run `docker compose up -d` to apply the change. `SHAPPYCHAT_BIND=127.0.0.1` publishes the backend port only to this computer, so the proxy is the only route in. The proxy must forward to `127.0.0.1:8849`, or to the value of `SHAPPYCHAT_PORT`. Caddy obtains and renews the certificate for the host name. Caddy forwards the original Host header by default: ```caddyfile chat.example.com { reverse_proxy 127.0.0.1:8849 } ``` With nginx, provide the certificate yourself. Forward the original Host header with `$http_host`, which keeps any port. Turn off response buffering and allow long reads, because the app streams events to the browser: ```nginx server { listen 443 ssl; server_name chat.example.com; ssl_certificate /etc/ssl/certs/chat.example.com.pem; ssl_certificate_key /etc/ssl/private/chat.example.com.key; location / { proxy_pass http://127.0.0.1:8849; proxy_set_header Host $http_host; proxy_http_version 1.1; proxy_buffering off; proxy_read_timeout 1h; proxy_send_timeout 1h; } } ``` The backend accepts a request only when its Host header matches an allowed host. The allowed hosts are the public URL host, including its port when it is not the default, and the local addresses on this computer. A proxy that rewrites the Host header causes a 403 response. When `SHAPPYCHAT_PUBLIC_URL` uses `https`, the backend marks the operator cookie `Secure`. Thus you must sign in through the HTTPS address. ## Upgrade Each release has its own image tag. Stop the stack, take a backup, then move to the new version: ```bash docker compose stop ``` Make a backup as [Backup](#backup) tells you. Then do these steps: 1. Download the files of the new release. 2. Check the files with `SHA256SUMS`. 3. Load the two image archives of the new release with `docker load -i`. 4. Compare the new `.env.example` with your `.env`. Add the new variables to `.env`. 5. Set the new version: ```dotenv SHAPPYCHAT_VERSION= ``` ```bash docker compose up -d docker compose ps ``` The data directory records its format in `data-version.json`. This release writes format 1. If a backend starts with data written by a newer build, it refuses to start and reports: ```text This data was written by a newer Shappy Backend (format N). Upgrade Shappy Backend or restore a backup. ``` Replace `N` with the format number in the message. To return to an older version, restore a backup taken before the upgrade. Do not run an older image against data from a newer one. ## Backup The release download includes `backup.sh` and `restore.sh`. Run them on the machine that runs the bundle, from the directory that holds them, and keep the archive private. Both scripts run the small `alpine:3` image to read and write the volumes, so an offline host must load that image first. Stop the project before a backup, so the copy is consistent. The compose project name is `shappychat` by default, from the `name:` entry in `compose.yml`. ```bash docker compose -p shappychat stop ./backup.sh shappychat ~/backups/shappychat-2026-10-09.tar.gz docker compose -p shappychat up -d ``` `./backup.sh ` archives the `data` and `celld-state` volumes into one file with mode 600. It refuses to overwrite an existing file, so choose a new name for each backup. The archive contains `master.key`, every stored provider and MCP secret, and all conversations. Store it outside the repository, with owner-only permissions, and do not share it. Restore into a stopped project: ```bash docker compose -p shappychat stop ./restore.sh shappychat ~/backups/shappychat-2026-10-09.tar.gz docker compose -p shappychat up -d ``` `./restore.sh [--force] ` creates any missing volumes and restores the archive into them. It refuses to run while the project has running containers. It refuses to write into volumes that already hold data, unless you pass `--force`, which replaces their content with the archive. Use a different project name to restore into new volumes without touching the current ones. The archive includes `master.key`, so a restore brings back the key that encrypts the stored secrets. If you set `SHAPPYCHAT_MASTER_KEY`, the backend uses this value and not `master.key`. This value must be the key that encrypted the data. ## Troubleshooting **(a) The browser shows "Unexpected runtime host" with status 403.** The Host header does not match an allowed host. Set `SHAPPYCHAT_PUBLIC_URL` to the address in the browser, including the scheme and the port, for example `http://localhost:8849`. Run `docker compose up -d` after the change. If a proxy is in front, check that it forwards the original Host header. **(b) The public address check reports "Local only".** The public URL is `localhost`, `127.0.0.1` or `[::1]`. That works on this computer only. For other computers, set `SHAPPYCHAT_PUBLIC_URL` to an `https` address and put a reverse proxy in front, as described in [HTTPS reverse proxy](#https-reverse-proxy). A public URL that uses `http` on another host is reported with "Use an https public URL behind a reverse proxy". **(c) The celld check fails.** The message reads "celld did not respond at ". Check the service and its log: ```bash docker compose ps docker compose logs celld ``` If the log shows "Waited 120 seconds for the backend tokens", the backend did not write its service tokens. Read `docker compose logs backend`, fix the backend start, then restart celld with `docker compose restart celld`. **(d) The model test fails from Docker.** A container cannot use `localhost` or `127.0.0.1` for a model on your computer. Set `SHAPPYCHAT_MODEL_URL` to `http://host.docker.internal:1234/v1` for LM Studio or `http://host.docker.internal:11434/v1` for Ollama. The model server must also accept connections from outside itself. Check its listen address if the test still fails. The tests of the bundle do not include this check. **(e) The backend reports "Cannot decrypt ... wrong master key or damaged file".** A different master key encrypted the file. Restore the `master.key` that matches the data, from the backup or the original installation, into the data volume. Alternatively set `SHAPPYCHAT_MASTER_KEY` to the base64 key that encrypted the data. Do not start a new installation over the data. Without the key, you cannot recover the stored secrets. **(f) The sign-in link has expired.** A link is valid for 10 minutes. You can use it one time only. Run `docker compose exec backend shappy login` again. **(g) Where the backend keeps secrets.** The backend encrypts the provider keys and MCP keys in the app settings with AES-256-GCM and `master.key` in the data volume. To supply the key from outside the volume, set `SHAPPYCHAT_MASTER_KEY`. The celld service tokens are in plain files: `celld.json` in the data volume and `celld.env` in the celld config volume. The backend writes both files with mode 0600. The backend keeps only hashes of app credentials. Keep backups and `.env` private, because `.env` contains the model key. --- Source: /docs/backend (Markdown: /docs/backend.md) # Shappy Backend Shappy Backend is the control plane for all the chats that you run. It keeps the model settings, the app tools and the access settings of each connected app. It runs each conversation in celld. Clients are the embedded chats in your apps and the Chrome extension. > **Note:** To run the backend with Docker, follow [Self-host](/docs/self-host). This page describes the parts of the backend and how to run it from the source checkout. ## How a request travels ![A message goes from the page to your app server, which adds the app credential; Shappy Backend checks the origin, the conversation scope and the tool grants; a celld cell runs the turn, calls the model server and your MCP tools, and streams events back](/docs/diagrams/request-flow.svg) 1. The chat in your page sends the message to your own server. 2. Your server forwards `/shappy/*` to the backend with the app credential. The browser never holds the credential. 3. The backend checks the origin, the conversation scope of the app and the tools that the app may use. 4. A celld cell runs the turn. It asks the backend for the model key at each model request. 5. The model answers. MCP tool calls go to the tools that you connected. 6. Text, tool results and status stream back to the page as server-sent events. 7. The conversation stays in celld. ## Start the backend Requirements: Node.js 22.19 or newer, the installed dependencies of the checkout, and Docker. The backend keeps conversations in celld, which runs in Docker. ```bash ./app.sh build ``` ```bash ./app.sh celld ``` ```bash ./app.sh start ``` `./app.sh celld` makes the service tokens, writes them to `celld.json` in the data folder and to the cell project, and starts celld 0.6.1 on `http://127.0.0.1:4610`. Run it again after you change `SHAPPYCHAT_PORT`. `./app.sh start` stops with a message if celld is not prepared. The backend listens on `http://127.0.0.1:8849`. To use another port, set `SHAPPYCHAT_PORT`. ## Sign in as the operator When the backend starts, it opens a private, single-use operator link in your browser. The link signs you in to the backend pages. - To get a new link, run `shappy login` on the same machine. In Docker: `docker compose exec backend shappy login`. From the source checkout: `node server-dist/cli.js login`. - If the browser does not open, open the URL in the `launch-url` file in the backend data folder. - The link expires after 10 minutes. - An operator session lasts 12 hours. It ends when you sign out or restart the backend. ## Backend pages | Page | Use it to | | --- | --- | | **Setup** | Follow the seven setup steps. A new installation opens here until setup is complete. | | **Playground** | Chat as the operator with every connected tool | | **History** | Read the Playground conversations and, under **All apps**, the conversations of every connected app | | **Models** | Set the model endpoint and choose models | | **Connected apps** | Add an app or the extension, set its origin and tools, copy or revoke its credential | | **Tools / MCP** | Add and refresh MCP connections | | **Storage** | See what the data folder holds | | **Operator access** | See the operator session and sign out | | **Preferences** | Other backend settings | ## Setup steps 1. **Check this server:** celld and storage. 2. **Model:** where answers come from. Presets for LM Studio, Ollama and other endpoints. 3. **Tools:** turn on the MCP connections that you added under **Tools / MCP**. 4. **Connect an app:** the credential and the allowed origin. 5. **Test message:** a message through the whole path. 6. **Add to your app:** the code, or a prompt for your coding agent. See [Set up with your agent](/docs/agents). 7. **Ready:** a summary. > **Warning:** Keep the `launch-url` file private. Anyone with the link can operate the backend. ## Configure a model Open **Models** and set the inference endpoint. The backend works with servers that supply an OpenAI-compatible API, for example a local LM Studio server at `http://127.0.0.1:1234/v1`. The backend finds the available models. It does not show embedding models. Each conversation keeps its model until you select a different one. ## Connect app tools Open **Tools / MCP** and add the MCP URL and key of your app. For local processes, use a configuration file. See [App tools (MCP)](/docs/mcp). ## Add a client Each app and each Chrome extension that uses the backend needs its own credential. 1. Open **Connected apps**. 2. Add a client. Enter its exact origin, for example `https://app.example.com` or `chrome-extension://`. 3. Select the MCP connections that the client can use. 4. If the client must act on its pages, grant host UI permission. 5. Copy the credential. The backend shows it one time only. It stores only a hash of the credential. Other clients and the Playground cannot see the conversations of a client. To remove access, revoke the client. Revoking also cancels its running replies. ## Allow your app origin The backend accepts browser requests only from origins that you allow. Set the list in the environment or in the configuration file: ```bash SHAPPYCHAT_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com ./app.sh start ``` ```json { "allowedOrigins": ["https://app.example.com"] } ``` Requests also need a valid credential. ## Conversations API All requests need the app credential as a bearer token. | Request | Result | | --- | --- | | `POST /api/conversations` with `{}` | Creates a conversation. The response contains `conversationId`. | | `GET /api/conversations` | Lists the conversations of the client | | `GET /api/conversations//events` | Server-sent events: the conversation state at connect, then each change. The event ID is the state revision. Send `Last-Event-ID` to receive only newer state. | | `GET /api/conversations/events` | Server-sent events for the list of conversations of the client | | `GET /api/models` | The models that the client can choose | | `GET /sdk/shappychat.tgz` | The SDK package file. No credential is needed. | The account and workspace scope comes from the trusted configuration. It does not come from the request body. ## Configuration reference | Variable | Description | | --- | --- | | `SHAPPYCHAT_PORT` | The port. Default `8849`. | | `SHAPPYCHAT_HOST` | The listen address. Default `127.0.0.1`. | | `SHAPPYCHAT_PUBLIC_URL` | The address that users reach, for example `https://chat.example.com` | | `SHAPPYCHAT_ALLOWED_HOSTS` | Extra allowed `Host` header values, comma-separated | | `SHAPPYCHAT_DATA_DIR` | The data folder. Default: `~/Library/Application Support/ShappyChat` on macOS, `%APPDATA%\ShappyChat` on Windows, `$XDG_DATA_HOME/shappychat` or `~/.local/share/shappychat` on Linux | | `SHAPPYCHAT_CONFIG` | Path to a JSON configuration file | | `SHAPPYCHAT_ALLOWED_ORIGINS` | Comma-separated list of allowed browser origins | | `SHAPPYCHAT_MODEL_URL` | The model endpoint. Default `http://127.0.0.1:1234/v1`. | | `SHAPPYCHAT_MODEL_API_KEY` | The key for the model endpoint. Secret. | | `SHAPPYCHAT_MASTER_KEY` | Base64 32-byte key that encrypts stored secrets. Default: `master.key` in the data folder. | | `SHAPPYCHAT_SDK_TARBALL` | Path of the SDK package file that `/sdk/shappychat.tgz` serves | ## Data folder | File | Contents | | --- | --- | | `celld.json` | The celld address, service token and callback token | | `launch-url` | The private operator link | | `clients.json` | Client records with hashed credentials | | `connections-.json` | Provider and MCP settings for one scope. Keys are encrypted. | | `master.key` | The key that encrypts provider and MCP keys. Back it up. | | `admin-secret` | The secret that `shappy login` uses to ask for a new operator link | | `data-version.json` | The format of the data folder | celld keeps the conversations and the request receipts. It has one cell for each conversation and one directory cell for each scope. It also has one ledger cell that lists the conversations of all scopes. **History** shows the conversations of the Playground. Under **All apps**, it shows the conversations of each connected app. Both lists change immediately when a conversation changes. celld keeps its data in `cells/shappy-backend/.celld` in the checkout. > **Warning:** The backend keeps the provider keys and MCP keys that you enter in the data folder. It encrypts them with AES-256-GCM and `master.key` or `SHAPPYCHAT_MASTER_KEY`. Cells do not keep these keys. A cell gets the key from the backend for each model request. Make a backup of `master.key` with the data folder. Without `master.key`, the backend cannot read the keys. --- Source: /docs/embed (Markdown: /docs/embed.md) # Embed the chat Add ShappyChat to your web app with the SDK. The SDK supplies the chat UI. A runtime does the work. See [Runtimes](/docs/runtimes). ## Get the SDK files The SDK package is `@shappychat/chat`. It is not on npm. Your Shappy Backend supplies it. In the folder of your app, run this command: ```bash npm install https://chat.example.com/sdk/shappychat.tgz ``` Use the public URL of your backend. Step 6 of the backend setup shows this command with your URL. The release also has this file as `shappychat-.tgz`. If you use TypeScript, set `"skipLibCheck": true` in your `tsconfig.json`. The SDK types refer to a package that the private file does not include. To build the SDK from the source checkout: ```bash ./app.sh build ``` The build writes the SDK to `sdk-dist/`. Link that folder in your project, or serve these files with your app: | File | Contents | | --- | --- | | `sdk-dist/embed.js` | The browser entry point | | `sdk-dist/style.css` | The chat styles. They apply only inside the chat, so they do not change the styles of your page. | | `sdk-dist/node.js` | The Node entry point (`@shappychat/chat/node`) | ## Mount the chat panel Use `mountShappyChatDrawer`. It supplies the full chat surface with the ShappyChat header and launchers. ```ts import { mountShappyChatDrawer, RemoteShappyChatRuntime } from '@shappychat/chat'; import '@shappychat/chat/style.css'; const runtime = await RemoteShappyChatRuntime.connect({ url: backendUrl, token: appCredential, conversationId, }); const drawer = mountShappyChatDrawer(document.body, { runtime, presentation: 'drawer', trigger: chatButton, onOpenChange: (open) => chatButton.setAttribute('aria-expanded', String(open)), }); chatButton.addEventListener('click', drawer.toggle); ``` > **Warning:** Do not put an app credential in a public bundle. Get it from your server for the signed-in user. To mount only the conversation in a container that you own, use `mountShappyChat(container, { runtime })`. ## Choose a presentation ![Four small windows: drawer, a panel on the right; floating, a window in the corner; island, a message bar at the bottom; inline, the chat fills a container in your layout](/docs/diagrams/presentations.svg "The four presentations") | Value | Closed | Open | | --- | --- | --- | | `drawer` (default) | Hidden | A panel on the right side of the window | | `floating` | A round button in the bottom-right corner | A floating chat window | | `island` | A round button at the bottom centre | A message bar. The conversation shows above it when it has messages. | | `inline` | Hidden | The chat fills a container in your layout | In the island, the ghost button folds the conversation. While the conversation is folded, a new message does not open it. Above the message bar, the latest status and the full reply show. A long reply scrolls in that area. Click it, or its chevron, to open the conversation. When the island is closed, its ghost button has no background and half opacity. Thus it does not hide the content of the page. Change the presentation at any time. The conversation and the unsent text stay. ```ts drawer.setPresentation('island'); ``` ## Control the drawer | Method | Description | | --- | --- | | `show()`, `hide()`, `toggle()` | Open or close the chat. Closing does not stop a running reply. | | `isOpen()` | Returns `true` if the chat is open. | | `getDraft()`, `setDraft(text)` | Read or set the unsent message. | | `setRuntime(runtime)` | Show another conversation in the same panel. | | `setPresentation(value)` | Change the presentation. | | `setTheme(theme)` | Change the colours. | | `unmount()` | Remove the UI. The runtime continues. | ## Change the colours The default theme is ShappyChat purple, in light and dark mode. Set colours with the `theme` option or with `setTheme`. ```ts const drawer = mountShappyChatDrawer(document.body, { runtime, theme: { accent: '#5ed6b4', glow: false }, }); ``` You can also set CSS custom properties on an ancestor element: ```css .my-app { --shappychat-accent: #5ed6b4; --shappychat-panel: #f7f7f2; --shappychat-field: #e9e9e1; --shappychat-ink: #1f2420; --shappychat-ghost: #1f2420; } ``` | Property | Changes | | --- | --- | | `--shappychat-accent` | Buttons, links and highlights | | `--shappychat-panel` | The chat background | | `--shappychat-field` | The message field and controls | | `--shappychat-ink` | The text colour | | `--shappychat-ghost` | The fill of the ghost mark | The chat uses dark mode when an ancestor element has the `dark` class. ## Fit the chat to your layout | Property | Default | Description | | --- | --- | --- | | `--shappychat-top` | `0px` | Set this to the height of your header. The panel starts below it. | | `--shappychat-width` | `440px` | The width of the drawer panel | Your app controls its own routing and layout. For example, when the drawer opens, you can add space on the right side of your content. ## Drawer options | Option | Description | | --- | --- | | `runtime` | The runtime of the conversation. Required. | | `presentation` | `drawer`, `floating`, `island` or `inline` | | `initialOpen` | Open the chat when it mounts. Default `false`. | | `trigger` | Your chat button. Focus goes back to it when the chat closes. | | `intro` | An element that shows above the conversation | | `showHeader` | Set `false` if your app supplies its own toolbar. | | `theme` | Colour overrides | | `onOpenChange(open)` | Called when the chat opens or closes | | `onNewChat()` | Called when the user starts a new chat | | `onModelSelect(anchor)` | Called when the user selects the model button | | `onError(error)` | Called when an error occurs | | `shortcut` | The key that your app binds to open the chat, shown in the island and the launcher. Default `{ label: '⌘/Ctrl K', title: 'Command/Ctrl K' }`. The SDK shows the key. Your app must handle it. | | `enableAttachments` | Allow file attachments. Default `true`. The chat sends images only to a model that reads images. | | `showExecutionMode` | Show the Auto, UI and MCP menu. Default `false`; the chat uses Auto. | ## Keyboard - **Enter** sends the message. **Shift + Enter** adds a new line. - **Escape** closes the chat. If a menu is open, Escape closes the menu first. - **Shift + Tab** in the message field changes the execution mode when `showExecutionMode` is on. See [App tools (MCP)](/docs/mcp). ## Let users choose a model Use `createShappyChatModelPicker`. It opens a menu beside the model button, in the width of the chat. ```ts import { createShappyChatModelPicker, mountShappyChatDrawer } from '@shappychat/chat'; const picker = createShappyChatModelPicker({ listModels: () => loadModelsFromYourServer(), onError: showError, }); const drawer = mountShappyChatDrawer(document.body, { runtime, onModelSelect: (anchor) => anchor && picker.open(anchor, runtime), }); ``` Close the picker before you replace or remove the runtime: `picker.close()`. --- Source: /docs/chrome (Markdown: /docs/chrome.md) # Chrome extension The Chrome extension puts ShappyChat in the Chrome side panel. The chat can read the page in the current tab and act on it. ## Install the extension The extension needs Chrome 141 or newer. It is not in the Chrome Web Store. Download `shappychat-extension-.zip` from the release. Unzip the file and go to step 1. Or, build the extension from the source checkout: ```bash node scripts/build.mjs ``` 1. In Chrome, open `chrome://extensions`. 2. Turn on **Developer mode**. 3. Click **Load unpacked**. Select the unzipped folder, or the `dist/` folder of the checkout. 4. Click **Details** on the ShappyChat extension. 5. Turn on **Allow user scripts**. The chat cannot act on pages without this permission. 6. Optional: turn on **Allow access to file URLs**. ## Sign in When you open the extension for the first time, it shows **Sign in to ShappyChat**. Choose where your chats are kept. | Choice | Models and history | Setup | | --- | --- | --- | | Use in this browser (default) | This browser keeps the chats (IndexedDB). You do not need a server. | Sign in to an AI provider. | | Connect to a backend | Shappy Backend keeps the models and the conversation history. | Connect the extension to a running backend. | You can change the choice later in **Settings**. Each choice keeps its own chat history. A change does not move or delete chats. ### Use in this browser Select one of these options: - **Continue with Claude** or **Continue with ChatGPT**: sign in with your account in the provider's sign-in window. - **API key**: paste a Claude (`sk-ant-`), OpenAI (`sk-`) or Gemini (`AIza`) key. - **Local model**: the extension finds LM Studio on port 1234 or Ollama on port 11434 on your computer. Load a chat model first. The sign-in details and the chats stay in this browser. To remove the sign-in, open **Settings** and click **Sign out**. Your chats stay. ### Connect to Shappy Backend 1. Start Shappy Backend and configure a model. See [Shappy Backend](/docs/backend). 2. On the sign-in page, click **Connect to a backend**. The form shows the extension origin, for example `chrome-extension://`. 3. In the backend, open **Connected apps**. Add an app with this exact origin. 4. Grant the MCP connections that the chat needs. Grant host UI permission if the chat must act on pages. 5. Copy the app credential. In the extension, enter the backend URL and the credential. The extension keeps the credential and the current conversation in extension storage. ## Choose the page that the chat uses The page control above the message field has three options: | Option | Behaviour | | --- | --- | | Current browser tab (default) | Before each message, the chat finds the active tab. It sends the page title and URL to the model as context. | | Pinned page | The chat always uses the page that you select. | | Chat only | The chat does not read or change any tab. | When a reply starts, the chat stays on its page until the reply is complete. If you change tabs during a reply, the work does not move to the new tab. ## Choose the chat appearance Open the appearance menu in the chat header. | Appearance | Description | | --- | --- | | Side panel | The chat is beside the current website. | | Floating | A compact chat window over the website. | | Touch Island (default) | A message bar on the website with a conversation that you can expand. | To close the Touch Island, click a different area of the website. The island becomes a faint ghost. To open it again, click the ghost. Your unsent text stays when you change the appearance. Floating and Touch Island chats stay on the tab when you go to another page, open or closed as before. ### Attach files Use the paperclip, paste an image or drop files on the message field. If the model can read images, the extension sends the images. For other models, attach documents, for example PDF, Word, PowerPoint, Excel or text files. The extension sends these documents as text. If you connected LM Studio or Ollama before this version, sign out. Then choose **Local model** again. Vision models then accept images. ## Keyboard - **Command + Shift + K** (macOS) or **Ctrl + Shift + K** opens the chat and puts the cursor in the message field. It works on any website. Command or Ctrl + K stays free for the website. - **Command + Shift + S** (macOS) or **Ctrl + Shift + S** opens or closes the side panel. - To change the key, open `chrome://extensions/shortcuts`. ## Permissions and data The extension asks for these Chrome permissions: - storage and unlimited storage - active tab, scripting and user scripts - side panel - declarative net request - access to http and https sites The browser tools use these permissions. - Page content that the chat reads goes to the model provider that you configured. - When you use the extension in this browser, the history stays in this browser. - When you connect to a backend, the backend keeps the conversation history. - A connection to an MCP server does not copy the extension history to that app. ## Limits - The chat can run browser JavaScript and use tagged page controls. - The chat can call MCP tools. With a backend, it uses the tools that the backend grants. In this browser only, it uses the MCP connections of the classic extension. - **Stop** cancels the reply. Stop cannot undo a script that already ran. --- Source: /docs/runtimes (Markdown: /docs/runtimes.md) # 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. --- Source: /docs/durable-chat (Markdown: /docs/durable-chat.md) # Durable Chat Use Durable Chat when a server must keep a conversation and recover permitted work after a host restart. ShappyChat supplies the chat UI and a backend adapter. Pi Durable runs the conversation in a celld host. Your app keeps its own records and supplies its MCP tools. > **Status:** The adapter is in the source checkout. The SDK is not on npm. A working integration also needs a configured celld host, model account and app access checks. The Notes demo does not start this host. ## Choose the connection | Part | Responsibility | | --- | --- | | Browser | Shows the shared ShappyChat UI through `RemoteShappyChatRuntime` | | Trusted backend | Checks access and binds the account, workspace and client to the correct cell | | `CelldShappyChatRuntime` | Sends text to the bound cell and reads its snapshots | | celld host with Pi Durable | Stores the conversation, admits input and runs work under its recovery policy | | App MCP server | Checks current permission and executes app operations | Keep the cell service token in the trusted backend. Do not import `CelldShappyChatRuntime` into browser code. Scope strings in an adapter are not access checks. Your backend must check that the signed-in user can use the bound cell. The adapter can also connect directly from a trusted Node process. Use the bridge when the browser must use the existing remote runtime protocol. ## Get the source SDK Use Node.js 22.19 or newer. From the ShappyChat checkout, install the pinned dependencies and build the SDK: ```bash npm ci ./app.sh build ``` The build reads `app/sdk.ts` and writes the Node entry to `sdk-dist/node.js`. This entry exports `CelldShappyChatRuntime`, `CelldRuntimeOptions` and `createRuntimeBridge`. The browser entry is `sdk-dist/embed.js`. The styles are in `sdk-dist/style.css`. The examples below use these local files. They do not require a published package. If you receive an SDK package file, verify its version and exports before use. A prior package file can have different exports. ## Connect a trusted backend directly Save this file as `durable-client.ts` in the checkout root. Set the URL to one provisioned cell route. Obtain the token from your private backend configuration. ```ts import { CelldShappyChatRuntime } from './app/sdk.js'; function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Set ${name}`); return value; } const runtime = await CelldShappyChatRuntime.connect({ url: required('DURABLE_CELL_URL'), token: required('DURABLE_CELL_TOKEN'), conversationId: required('DURABLE_CONVERSATION_ID'), scope: { accountId: required('DURABLE_ACCOUNT_ID'), workspaceId: required('DURABLE_WORKSPACE_ID'), }, }); try { await runtime.submit({ requestId: required('DURABLE_REQUEST_ID'), message: 'List the records that I can read.', }); console.log(runtime.getSnapshot().status); } finally { await runtime.close(); } ``` Run it with the installed TypeScript loader: ```bash node --import tsx durable-client.ts ``` These variable names belong to this example. They are not automatic ShappyChat host settings. Use the same request ID and message when you retry an uncertain submission. Do not create a new ID until you know the state of the original request. The `conversationId` and `scope` options label the ShappyChat snapshot. They do not select or authorize a cell. The `url` selects the cell. An optional `clientId` requires the cell snapshot to report the same client grant ID. `submit()` sends the input and waits for pending work to end. A terminal failure can cause this method to throw. Keep the stored snapshot and original request ID available for inspection. ## Connect the browser through the bridge Save the backend file as `durable-bridge.ts` in the checkout root: ```ts import { createRuntimeBridge } from './app/sdk.js'; function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(`Set ${name}`); return value; } const bridge = await createRuntimeBridge({ storageDirectory: required('DURABLE_BRIDGE_DATA'), port: 8849, token: required('DURABLE_APP_TOKEN'), allowedOrigins: [required('DURABLE_APP_ORIGIN')], scope: { accountId: required('DURABLE_ACCOUNT_ID'), workspaceId: required('DURABLE_WORKSPACE_ID'), }, celld: { url: required('DURABLE_HOST_URL'), token: required('DURABLE_CELL_TOKEN'), }, }); console.log(bridge.url); process.once('SIGINT', () => void bridge.close()); process.once('SIGTERM', () => void bridge.close()); ``` ```bash node --import tsx durable-bridge.ts ``` This example starts a loopback bridge for one trusted scope. It does not supply public multi-user sign-in. The app token and the cell token are separate credentials. Obtain the app token at runtime through your authenticated app. Do not include either token in a public bundle. The bridge appends `/bots/` to `DURABLE_HOST_URL`. It computes the cell ID from the runtime scope and ShappyChat conversation ID. The host must supply this route and bind it to the correct cell. The bridge does not install celld or configure a model. Do not map all these routes to one shared cell. For external clients in operator mode, set `celld.clientBindings` with a separate `{ url, token }` entry for each registered client ID. The client must have the `accounts` connection grant. Configure the cell host with the same client ID and its scoped App MCP token. A shared operator token does not establish this grant. Do not grant host UI access to a celld client. The bridge keeps conversation membership in its SQLite database. The cell keeps the messages. When you set `celld`, the bridge does not convert the local history that you have. The bridge rejects local history without the matching cell authority record. Use a separate, verified history import. ### Mount the shared chat Save the frontend example at the checkout root. Bundle the browser SDK with your app. Supply `backendUrl` and `appCredential` through your authenticated app configuration. ```ts import { RemoteShappyChatRuntime, mountShappyChatDrawer, } from './sdk-dist/embed.js'; import './sdk-dist/style.css'; export async function openDurableChat( backendUrl: string, appCredential: string, ) { const response = await fetch(`${backendUrl}/api/conversations`, { method: 'POST', headers: { Authorization: `Bearer ${appCredential}`, 'Content-Type': 'application/json', }, body: '{}', }); if (!response.ok) throw new Error('Cannot create the conversation'); const { conversationId } = await response.json(); const runtime = await RemoteShappyChatRuntime.connect({ url: backendUrl, token: appCredential, conversationId, }); const drawer = mountShappyChatDrawer(document.body, { runtime, presentation: 'drawer', enableAttachments: false, }); drawer.show(); return { drawer, runtime, conversationId }; } ``` Call `openDurableChat()` with the authorized bridge URL and app credential. Save the returned conversation ID in your app. On the next visit, connect with that ID instead of creating another conversation. The bridge must allow the exact app origin. See [Shappy Backend](/docs/backend) and [Security and data](/docs/security) for client access rules. ### Keep your own chat UI Your app can keep its current layout and controls. Connect `RemoteShappyChatRuntime` to the bridge. Use `getSnapshot()` for the initial state and `subscribe()` for changed state. Send text with `submit({ requestId, message })`. Use `cancel()` for an explicit Stop action. A ShappyChat drawer is optional. ```ts import { RemoteShappyChatRuntime } from './sdk-dist/embed.js'; export async function connectCustomChat( url: string, token: string, conversationId: string, showMessages: (messages: unknown[]) => void, ) { const runtime = await RemoteShappyChatRuntime.connect({ url, token, conversationId, }); showMessages(runtime.getSnapshot().messages); const unsubscribe = runtime.subscribe((state) => showMessages(state.messages)); return { send: (requestId: string, message: string) => runtime.submit({ requestId, message }), stop: () => runtime.cancel(), close: async () => { unsubscribe(); await runtime.close(); }, }; } ``` Use your renderer for the returned messages. Show running, interrupted and error states from the snapshot. Keep unsent text in your UI. Preserve the original request ID when the response to a submission is uncertain. ## Host contract and configuration The adapter expects these service actions relative to the bound cell URL: | Action | Input | Result | | --- | --- | --- | | `GET snapshot` | No body | Cell conversation ID, model, agent settings, messages, pending count and submission state | | `POST submit` | `{ requestId, message }` with text input | HTTP 202 admission receipt from the plugin host | | `POST cancel` | `{}` | Cancellation result | Each request uses the cell service bearer token. Admission confirms that the cell stored the input. It does not confirm a model response. For the current `PluginConversationCell` host, configure the account ID, provider URL, model ID, wire API, service token and conversation definition ID. Configure the App MCP URL and its scoped token. Keep supported definitions available for unfinished work. The host can supply provider keys and retained definitions. Client-bound cells also need the matching client grant ID. These are host responsibilities. The adapter does not discover models, grant Bot access or provision Workers. Use your host's deployment instructions for these operations. ### ShappyOS guarded submit is a different interface The ShappyOS guarded `POST /api/product/chat/submit` route takes `{ scopeId, botId, requestKey, text }`. It returns HTTP 200 with an admission receipt. It requires a principal with `chat.submit` capability and the Bot scope. The App management token does not grant this access. The cell adapter uses `{ requestId, message }`. Do not set its URL to the guarded route. The `celldChatPort` transport uses an `input` wrapper around the guarded body. Supply a compatible handler for that transport. The current guarded catalog has no public chat snapshot, stream, recovery or cancellation operation. The service actions above are internal host actions. They are not public App management routes. ## Updates, closure and recovery The cell adapter reads snapshots every 500 ms while it has subscribers. While it waits for a submission, it reads every 250 ms. The bridge sends changed snapshots to browsers through server-sent events. This is snapshot delivery. The adapter sets `streamMessage` to `null`. It does not provide model token streaming. | Action | Effect | | --- | --- | | `drawer.hide()` or `drawer.unmount()` | Closes or removes the UI. Accepted work continues. | | `runtime.close()` | Stops this client's updates. It does not call cell cancellation. | | `runtime.cancel()` or the chat Stop control | Sends an explicit cancellation request. | | Reconnect to the same bound cell | Reads the stored conversation. It does not resubmit the last message. | celld can wake stored work after a host restart. Recovery uses the saved conversation definition and its replay policy. A model request can repeat during recovery. An uncertain external operation can require inspection before a retry. Durable storage does not make every tool operation safe to repeat. ## Current limits - Text input only. The adapter does not support file attachments or appended context. - App MCP tools only. This adapter does not support browser tools, tagged UI controls or host UI tools. - Configure the model, reasoning and instructions at the host. The adapter rejects changes to these settings. - No automatic local-history migration or production cell provisioning. - No model token streaming. No public guarded observation or cancellation API in the current ShappyOS catalog. - This guide does not publish or deploy a standalone SDK. The Notes example uses a scripted provider by default. Its real MCP operations do not establish real model inference. Set a real model endpoint for inference tests. A successful SDK build or this documentation preview does not test host recovery or model execution. See [Embed the chat](/docs/embed) for presentation options. See [Runtimes](/docs/runtimes) for local alternatives. --- Source: /docs/mcp (Markdown: /docs/mcp.md) # App tools (MCP) The chat uses the operations of your app through the Model Context Protocol (MCP). Your app supplies an MCP server. The chat finds its tools and calls them. The chat does not copy your business logic or your data. ## Connect an HTTP MCP server In Shappy Backend, open **Tools / MCP**. Enter the MCP URL of your app and a bearer key with a limited scope. In a browser-local runtime, add the connection in code: ```ts const connections = new McpConnections(); await connections.add({ id: 'my-app', url: 'https://app.example.com/mcp', headers: { Authorization: `Bearer ${scopedKey}` }, }); ``` Use an `http:` or `https:` URL. Do not put a user name or password in the URL. The runtime starts the MCP session and reads all tools, one page at a time. For each call, it compares the input with the tool schema. Then it calls the tool. ## Connect a stdio MCP server Shappy Backend can start an MCP server as a local process. Put the connection in a private configuration file: ```json { "connections": [ { "transport": "stdio", "id": "my-app", "command": "/usr/local/bin/node", "args": ["dist/mcp.js"], "cwd": "/path/to/my-app", "inheritEnv": ["MY_APP_API_TOKEN", "MY_APP_URL"] } ] } ``` Start the backend with this file: ```bash SHAPPYCHAT_CONFIG=/path/to/shappychat.local.json ./app.sh start ``` - The command and the path come from your configuration. The model cannot change them. - `inheritEnv` lists the environment variables that the process gets. Supply secrets through the environment, not in the file. - Your app still checks the permissions of each operation. The backend also makes these tools available over HTTP at `http://127.0.0.1:8849/mcp`. Clients such as the Chrome extension can use them with a credential that grants the connection. ## Choose how the chat acts ![Three panels: in Auto the chat can use UI tools and MCP tools; in UI only UI tools; in MCP only MCP tools](/docs/diagrams/modes.svg "Execution modes") The message field shows an execution mode: **Auto**, **UI** or **MCP**. | Mode | Tools that the chat can use | Use it to | | --- | --- | --- | | Auto (default) | UI tools and MCP tools | Let the chat choose. It uses app operations for data and the UI for visible changes. | | UI | UI tools only | Open forms, fill fields and save through the controls of your app. | | MCP | MCP tools only | Read or change data directly. | - Click the mode to open the menu. Or press **Shift + Tab** in the message field. - The chat saves the mode for each conversation. - If you change the mode during a reply, the new mode applies to the next reply. - If the tools of a mode are not available, UI or MCP mode shows an error before the model starts. You can also set the mode in code: ```ts await runtime.configure({ executionMode: 'ui' }); const { executionMode, activeExecutionMode } = runtime.getSnapshot(); ``` > **Note:** The execution mode selects the tools. It is not a security sandbox. --- Source: /docs/ui-tools (Markdown: /docs/ui-tools.md) # UI control discovery You can let the chat use controls on your page, such as buttons, fields and lists. Mark each control that the chat can use. The chat does not use controls that you do not mark. ## Mark controls Add a stable, unique `data-agent-id` to each control. Add `data-agent-description` to tell what the control does and what changes it causes. ```html ``` The chat finds the type of action and the label from the element. The descriptions are app data. They are not instructions to the model. ## Connect the controls to the chat ```ts import { createDomAdapter } from '@shappychat/chat'; const ui = createDomAdapter({ root: document.querySelector('#app')! }); await runtime.attachUi(ui); // RemoteShappyChatRuntime // Optional: let a control wait for your existing operation. const unregister = ui.register('notes.save', { click: async () => saveExistingDraft(), }); // When your view closes: unregister(); await runtime.detachUi(); ui.dispose(); ``` For a browser-local runtime, give the controls to the runtime as tools: ```ts import { createUiTools } from '@shappychat/chat'; const tools = createUiTools(async (operation, request, signal) => operation === 'discover' ? ui.discover() : ui.act(request!, { signal })); runtime.setHostTools(tools); ``` ## What discovery returns For each marked control, discovery returns its visibility, its enabled state, the actions that it supports and its current value. - Discovery does not include password, file or hidden inputs. - If two controls have the same ID, discovery fails. - An action on a hidden, inert, disabled or unsupported control fails. - Discovery covers the root element that you supply. It does not cover frames from other origins or shadow trees. > **Tip:** A click does not prove that an operation is complete. For actions that save data, register a handler that waits for the operation. Then the chat can check the result. ## Show actions with a cursor The chat can show a pointer that moves to each control before it acts. Users can follow what the chat does. ```ts import { createDomAdapter, createUiCursor } from '@shappychat/chat'; const cursor = createUiCursor({ storageKey: 'my-app.shappychat.cursor' }); const ui = createDomAdapter({ root: document.querySelector('#app')!, cursor }); await runtime.attachUi(ui); cursor.setMode('follow'); // 'off' | 'fast' | 'follow' ``` | Mode | Behaviour | | --- | --- | | Off (default) | Actions occur immediately. | | Fast | Short movements and pauses | | Follow | Slower movements, so that users can watch | - Users can change the mode with the pointer button beside the execution mode. MCP mode hides this button. - With reduced motion, the cursor does not move or pause. - The cursor does not move the real mouse pointer. - **Stop** cancels an action that has not started. When your view closes, dispose of the cursor: `cursor.dispose()`. ## One UI owner for each conversation Only one tab can supply UI tools to a conversation. A second client for the same conversation can read it. The runtime does not keep UI actions. If the tab disconnects, the runtime cancels the waiting actions and does not send them again. --- Source: /docs/security (Markdown: /docs/security.md) # Security and data This page tells where ShappyChat keeps data and how to keep credentials safe. ![Where secrets live: the app credential stays on your app server; provider and MCP keys are encrypted in the backend data folder; cells ask the backend for a key at each model request; the browser holds only the user's sign-in](/docs/diagrams/security.svg) ## Who owns which data | Data | Owner | Location | | --- | --- | --- | | App records, for example notes | Your app | Your database | | Conversation history | ShappyChat runtime | celld cells behind Shappy Backend, browser IndexedDB or your Node folder | | App credentials | Shappy Backend | Hashes only, in `clients.json` | | Provider and MCP keys entered in the backend | Shappy Backend | Data folder, encrypted with AES-256-GCM under `master.key` or `SHAPPYCHAT_MASTER_KEY` | | Operator sign-in | Shappy Backend | A single-use link (10 minutes), then a 12-hour HttpOnly session cookie. The cookie is `Secure` when the public URL uses HTTPS. | | celld service tokens | Shappy Backend | `celld.json` in the data folder | | Chrome extension settings | Chrome extension | Extension storage | The chat does not add tables to your database. ## Keep credentials on the server - Do not put an app credential or an MCP key in a public JavaScript bundle. - Give each app and each extension its own credential. - Give each credential only the MCP connections that it needs. - Grant host UI permission only to clients that must act on pages. - Revoke a credential when you do not need it. Revoking cancels its running replies. - An app credential is a long-term secret. It is not a short pairing code. > **Warning:** A credential is for one app. It is not for one user. In a product that your customers use, your server must check the user session for each `/shappy/*` request. Your server must also make sure that each user gets access only to their own conversations. Version 0.1.0 of the backend does not have sessions for each user. ## Set the scope on the server Each conversation belongs to an account and a workspace. Set this scope from trusted configuration, for example the signed-in session. Do not take the scope from a chat message or from a request body. ## Requests and retries - Each message has a request ID. If the client sends the same request ID with the same message again, the runtime does not submit it twice. The runtime rejects a different message with the same ID. - The remote runtime does not send a message again automatically after a network error. - After a restart, a tool call that was running is shown as an error with an unknown result. It does not run again. > **Tip:** Make operations that change data idempotent in your app. Then a repeated call does not change data twice. ## What the model receives - The messages of the conversation - The results of the tools that the chat calls - In the Chrome extension: the title and URL of the page that the chat uses, and the page content that a tool reads Page context and control descriptions are data for the model. They are not instructions. The model provider that you configure receives this data. Read the data policy of your provider. ## Cancellation **Stop** cancels the reply and the tool calls that have not started. An operation that already started can complete in your app. Check the state of your app before you try again. --- Source: /docs/api (Markdown: /docs/api.md) # API reference The SDK has two entry points: - `@shappychat/chat`: the browser entry point - `@shappychat/chat/node`: the Node entry point Import the styles one time: `import '@shappychat/chat/style.css'`. ## Mount the UI | Export | Description | | --- | --- | | `mountShappyChatDrawer(container, options)` | Mounts the full chat surface with header and launchers. Returns a `ShappyChatDrawer`. See [Embed the chat](/docs/embed). | | `mountShappyChat(container, options)` | Mounts only the conversation and the message field. Returns `{ element, unmount }`. | | `createShappyChatModelPicker(options)` | Creates the model menu. Methods: `open(anchor, runtime)` and `close()`. | ### `MountShappyChatOptions` | Option | Type | Description | | --- | --- | --- | | `runtime` | `ShappyChatRuntime` | Required | | `onModelSelect` | `(anchor?: HTMLElement) => void \| Promise` | Shows the model button when set | | `onError` | `(error: unknown) => void` | Called when an error occurs | | `onBeforeSend` | `() => void \| Promise` | Called before each message is sent | | `enableAttachments` | `boolean` | Default `true`. The chat sends images only to a model that reads images. It sends documents as text to all models. | | `showExecutionMode` | `boolean` | Shows the Auto, UI and MCP menu and its Shift + Tab shortcut. Default `false` (Auto). | | `character` | `GhostCharacter` | The Shappy Ghost shown while a turn is in progress: `classic` (default), `sprout`, `bow`, `headphones`, `cap`, `wizard`, `blush`, `glasses`, `antenna`, `imp`, `snow` or `crown`. `GHOST_CHARACTERS` lists them with their colours. | ### Types | Type | Values | | --- | --- | | `ShappyChatPresentation` | `'drawer' \| 'floating' \| 'island' \| 'inline'` | | `ShappyChatTheme` | `{ accent?, panel?, field?, ink?, ghost?: string; glow?: boolean }` | | `ExecutionMode` | `'auto' \| 'ui' \| 'mcp'` | ## Runtimes | Export | Description | | --- | --- | | `RemoteShappyChatRuntime.connect({ url, token, conversationId })` | Connects to a conversation on Shappy Backend | | `createShappyChatRuntime(options)` | Opens or creates a browser-local Pi Durable conversation, stored in IndexedDB. Options: `scope`, `conversationId?`, `model?`, `systemPrompt?`, `connections?`, `uiCursor?`, `getApiKey?`, `database?`, `onMoved?`. One window at a time writes a conversation. Opening it elsewhere asks the holding window to hand it over: an idle holder closes it and calls its `onMoved`, a holder still answering refuses. | | `listBrowserConversations(scope)` | Lists the conversations stored in this browser for a scope, newest first | | `DurableRuntime` | The runtime class behind `createShappyChatRuntime` and the Node runtime | | `McpConnections` | A set of MCP connections. Use `add({ id, url, headers })`. | | `discoverOpenAiModels(baseUrl, apiKey?)` | Lists the chat models of an OpenAI-compatible endpoint | | `openAiCompatibleModel(id, baseUrl, name?)` | Describes one model on an OpenAI-compatible endpoint | ### Runtime methods | Method | Description | | --- | --- | | `getSnapshot()` | Returns the current state, including `conversationId`, `model`, `executionMode` and `activeExecutionMode` | | `configure(input)` | Changes settings, for example `{ executionMode: 'ui' }` | | `attachUi(adapter)`, `detachUi()` | Connects or removes UI tools (remote runtime) | | `setHostTools(tools)` | Sets UI tools (browser-local runtime) | | `close()` | Stops the updates of this client (remote runtime), or releases the conversation for other windows (browser-local runtime) | ## UI tools | Export | Description | | --- | --- | | `createDomAdapter({ root, cursor? })` | Finds marked controls in `root` and acts on them. Methods: `discover()`, `act(request)`, `register(id, handlers)`, `dispose()`. | | `createUiCursor({ mode?, storageKey? })` | Creates the visible cursor. Methods: `setMode(mode)`, `dispose()`. | | `createUiTools(executor)` | Turns a discover and act function into tools for a browser-local runtime | | `AGENT_ID_ATTRIBUTE` | `'data-agent-id'` | | `AGENT_DESCRIPTION_ATTRIBUTE` | `'data-agent-description'` | | `EXECUTION_MODES` | The list of execution modes | ## Node entry point | Export | Description | | --- | --- | | `createNodeShappyChatRuntime(options)` | Creates a runtime with Pi Durable SQLite storage in `storageDirectory`. Returns `{ runtime, host, close }`. | | `addStdioConnection(connections, config)` | Adds an MCP server that runs as a local process | | `openNodeDurableHost(directory, getApiKey?)` | Opens the Pi Durable host for `directory/chat.sqlite` and claims it for this process. Returns `{ host, close }`. | | `createRuntimeBridge(config)` | Serves a runtime over authenticated HTTP. Its optional `celld` setting connects to a bound cell host. | | `CelldShappyChatRuntime.connect(options)` | Source Node export for a trusted celld connection. See [Durable Chat](/docs/durable-chat). | | `CelldRuntimeOptions` | `{ url, token, conversationId, scope, clientId? }`. Keep the service token in the backend. | --- Source: /docs/troubleshooting (Markdown: /docs/troubleshooting.md) # Troubleshooting ## The model list is empty 1. Make sure that the model server runs. 2. Make sure that the endpoint ends in `/v1`, for example `http://127.0.0.1:1234/v1`. 3. Load a chat model. The model list does not show embedding models. ## The demo asks for a pairing key A new browser session needs the private pairing link. Open the URL in `~/.shappychat/example-notes/launch-url`. Or enter the pairing key in the connection form. ## "Tool unavailable" in UI or MCP mode - UI mode needs a tab that supplies UI tools. Open the app tab, or change to **Auto**. - MCP mode needs connected app tools. Connect the MCP server, or change to **Auto**. ## The chat cannot act on pages in Chrome Open `chrome://extensions`. Click **Details** on the ShappyChat extension. Turn on **Allow user scripts**. ## The browser blocks requests to the backend The backend accepts requests only from allowed origins. Add the origin of your app to `SHAPPYCHAT_ALLOWED_ORIGINS`. See [Allow your app origin](/docs/backend#allow-your-app-origin). ## The browser-local runtime cannot reach a server A browser-local runtime sends requests directly from the page. The MCP server and the model endpoint must accept requests from your origin (CORS). If they do not, use Shappy Backend. ## On a phone, the chat cannot use app controls A full-screen chat panel can cover the app and make its controls unavailable. Use the `floating` or `island` presentation. ## The operator link expired The operator link expires after 10 minutes. To get a new link, run `shappy login` on the same computer. In Docker, run `docker compose exec backend shappy login`. ## Docker and self-host problems For these problems, see [Self-host troubleshooting](/docs/self-host#troubleshooting): - a `403` response from the `Host` check - the health of celld - model URLs in containers - the data version check