# Hosting apps

> Run web apps inside a Botyard bot and share them through authenticated Bot Page URLs: persistent hosting, temporary previews, Streamlit, WebSockets, and caching.

Canonical URL: https://docs.botyard.io/docs/hosting-apps

Botyard bots can host web apps they build or manage. A bot runs the app on a local port inside its runtime, then shares it with you through an authenticated Bot Page URL.

Use this for:

* app previews while a bot is building something
* lightweight internal tools
* dashboards and reports
* Streamlit apps
* demos that combine a frontend, API, or WebSocket server
* small dynamic websites that a bot can keep running and repair when needed

Bot-hosted apps are practical and secure by default, but they are not a high-availability deployment. The app depends on the bot runtime, its workspace, and the local server process. For many team workflows, that tradeoff is exactly right: you can ask the bot to build, host, inspect, and fix the app in the same conversation.

## How hosting works

There are two pieces:

| Piece            | What it does                                                                                              |
| ---------------- | --------------------------------------------------------------------------------------------------------- |
| Runtime services | Keep local processes running inside the bot, such as `pnpm dev`, `next start`, or `streamlit run app.py`. |
| Bot Pages        | Publish one local loopback port as an authenticated HTTPS URL.                                            |

For a quick preview, the bot can start a local server and expose it for a short time. For something you want to keep using, ask the bot to register the app as a persistent service first, then expose it as a persistent Bot Page.

## Quick start

Ask your bot:

```text
Build a small Vite app, run it locally, and share it with me as a Bot Page.
Use a short-lived preview URL.
```

The bot should:

1. create or update the app in its workspace
2. start the web server on `127.0.0.1`
3. expose the server port with a Bot Page
4. send you the returned URL

An unauthenticated Bot Page URL redirects to Botyard sign-in. After sign-in, Botyard checks that you can access the bot before proxying traffic to the app.

## Persistent hosting

Use persistent hosting when the app should survive command completion or bot runtime restarts.

Ask your bot:

```text
Host this app persistently. Register the web server as a Botyard service with autostart,
then expose it as a persistent Bot Page.
```

The bot should use the runtime service tools to save a service definition. A typical Vite app looks like this:

```text
service_register(
  name="web",
  command="pnpm",
  args=["dev", "--host", "127.0.0.1", "--port", "5173"],
  cwd="my-app",
  autostart=true,
  restart_policy="on_failure",
  ports=[{"name":"http","port":5173,"kind":"web"}]
)
service_start(name="web")
expose_botpage(port=5173, name="my-app", kind="web_app", persistent=true)
```

The Bot Page stays available until it is revoked. If the app breaks, ask the bot to inspect `service_status`, `service_logs`, and the local port, then restart or repair the service.

## Temporary previews

Bot Pages default to a short TTL, currently 2 hours. That is best for previews and one-off checks:

```text
expose_botpage(port=5173, name="vite-preview")
```

Use a temporary page when:

* you only need to inspect the current build
* the server was started manually
* the app is experimental
* you do not want a long-lived URL

Use a persistent page when:

* the page is meant to be reused
* the bot should keep hosting it after restart
* other people will rely on the same URL

## Limits

| Limit                               | Value                                          |
| ----------------------------------- | ---------------------------------------------- |
| Default lifetime                    | 2 hours                                        |
| Longest lifetime with an expiry     | 30 days (`ttl_minutes=43200`)                  |
| Persistent page (`persistent=true`) | No expiry. The page stays until it is revoked. |
| Pages per port                      | One. Each Bot Page needs its own port.         |
| Reserved ports                      | 6080, 9090, 9091, 9093, 9222 and 18789         |

A page's lifetime can't be extended after it is exposed. To keep an app online longer, revoke
the page and expose it again with a longer `ttl_minutes`, or with `persistent=true`. The new
page may get a different URL.

A port can serve only one live Bot Page at a time. If the bot tries to expose a port that another
of its live pages already uses, Botyard refuses, because both URLs would then show whichever app
is listening on that port. Revoke the old page first, or run the new app on a different port.
Revoked and expired pages free their port.

The reserved ports belong to the Botyard runtime and the bot's built-in browser session, so a
bot can't expose them as Bot Pages. Run apps on other ports, such as the defaults below.

## Streamlit apps

Streamlit uses WebSockets and app-specific XSRF cookies. Botyard supports this with the `streamlit_app` Bot Page kind.

Ask your bot:

```text
Create a Streamlit dashboard and host it persistently as a Bot Page.
Use the Streamlit app kind so WebSockets work.
```

A reliable setup uses a virtual environment inside the workspace:

```text
python3 -m venv .venv
.venv/bin/python -m pip install streamlit
```

Then the bot registers and exposes the app:

```text
service_register(
  name="streamlit",
  command="/home/openclaw/.openclaw/workspace/my-streamlit/.venv/bin/streamlit",
  args=[
    "run",
    "app.py",
    "--server.address",
    "127.0.0.1",
    "--server.port",
    "8501",
    "--server.enableCORS",
    "false",
    "--server.enableXsrfProtection",
    "false",
    "--browser.gatherUsageStats",
    "false"
  ],
  cwd="my-streamlit",
  autostart=true,
  restart_policy="on_failure",
  ports=[{"name":"http","port":8501,"kind":"web"}]
)
service_start(name="streamlit")
expose_botpage(port=8501, name="streamlit", kind="streamlit_app", persistent=true)
```

Avoid virtual environments or generated files under `/tmp`; they disappear when the bot runtime restarts.

## Frontend plus API

For generated apps, the simplest convention is:

| Part           | Default          |
| -------------- | ---------------- |
| Frontend       | `127.0.0.1:5173` |
| API            | `127.0.0.1:3001` |
| API path       | `/api`           |
| WebSocket path | `/ws`            |

When possible, expose one public Bot Page for the frontend and have the frontend call same-origin paths such as `/api` and `/ws`. That keeps auth, cookies, and browser origins simple.

If the frontend and API cannot be rewritten to share one origin, the bot can expose more than one port. Each exposed port gets its own Bot Page and URL, and each port can back only one live Bot Page (see [Limits](#limits)). Use that only when the app really needs separate origins.

## WebSockets

Bot Pages proxy WebSocket traffic under the same authenticated URL as HTTP traffic. The browser must already have a valid Botyard session for the WebSocket request. For app frameworks such as Streamlit, Botyard preserves the WebSocket subprotocol and app cookies needed by the framework.

If a WebSocket fails:

1. ask the bot to confirm the local WebSocket server is running
2. check whether the app uses the same Bot Page origin
3. for Streamlit, confirm the page was exposed with `kind="streamlit_app"`
4. ask the bot to restart the service and inspect its logs

## Managing hosted apps

Useful requests:

```text
List my active Bot Pages.
```

```text
Show the services you have registered and their status.
```

```text
Restart the hosted app and check that the public URL still works.
```

```text
Take down the old preview URL.
```

The bot can revoke a page when it is obsolete, exposed on the wrong port, or no longer needed:

```text
revoke_botpage(exposure_id="...")
```

## Caching

Bot Pages are never cached by Cloudflare or any other shared cache. Every response is marked `private`, so only your own browser can keep a copy. The bot chooses how long it keeps one with a cache policy, either when it exposes the page or later on the live page:

| Policy             | Browser behaviour                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| `origin` (default) | Follows the app's own `Cache-Control` headers, and revalidates on every load when the app sends none |
| `revalidate`       | Always checks for a newer version, even when the app asks for long caching                           |
| `no_store`         | Never keeps a copy, for live dashboards or sensitive data                                            |

If you keep seeing an old version of a page after the bot changed it, ask:

```text
Set the Bot Page's cache policy to revalidate, then I'll reload.
```

The URL does not change. Pages your browser cached before the change may need one hard reload.

## Security

Bot Pages are authenticated. A user who opens the URL must sign in with Botyard, and Botyard checks access to the bot before forwarding traffic.

Important details:

* The app listens on loopback inside the bot runtime.
* The bot opens an outbound tunnel to Botyard; the local app is not directly exposed to the internet.
* Botyard session cookies are not forwarded to the hosted app.
* The app may receive forwarded identity metadata such as `x-forwarded-user`.
* You can revoke a Bot Page when it should no longer be reachable.

Because the app runs in the bot workspace, treat it like any other bot-authored artifact. Ask the bot what it is running, what port it exposed, and whether the page should be temporary or persistent.

## Troubleshooting

| Symptom                                                          | What to ask the bot                                                                                            |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| The page redirects to sign-in                                    | "This is expected if I am not authenticated. After I sign in, verify the page loads."                          |
| The page shows a 502                                             | "Check whether your tunnel is connected, the service is running, and the local port returns 200."              |
| It worked before a restart but not after                         | "Confirm the app is registered as a service with autostart and that dependencies are stored in the workspace." |
| Streamlit loads but interactivity fails                          | "Confirm the Bot Page kind is streamlit\_app and inspect the Streamlit service logs."                          |
| The page shows an old version                                    | "Set the Bot Page's cache policy to revalidate." Then hard-reload once.                                        |
| The wrong app appears                                            | "List Bot Pages and revoke stale exposures for old ports."                                                     |
| Exposing fails with "Port … is already served by the Bot Page …" | "Revoke the stale Bot Page on that port, or run the new app on a different port."                              |
| Exposing fails with "Port … is reserved"                         | "Run the app on a different port. That port is reserved for Botyard."                                          |
| The page says "Bot Page expired"                                 | "Expose the app again, persistently if I need to keep using it."                                               |

For persistent apps, a good recovery request is:

```text
Trace the hosted app end to end: list services, inspect logs, curl the local port,
list Bot Pages, and verify the public URL after auth.
```

## Related docs

* [Runtime Vault](/docs/runtime-vault)

For a product overview, see [Bot Pages on botyard.io](https://botyard.io/features/bot-pages).
