# kbagent web UI
Two ways to run the UI:
- **End-user / single-process** -- one Python command, no Node runtime needed:
`kbagent serve --ui`. FastAPI mounts the built React SPA on `/`, injects
the bearer token into `index.html`, and rewrites `/api/*` to bare API
routes. Use this for "just give me the UI" deployments.
- **Dev mode (3 terminals)** -- Vite dev server + Node BFF + kbagent serve.
Used when iterating on the React app: hot reload, source maps, the works.
See [Dev mode](#dev-mode-three-terminals) below.
## Single-process mode (recommended for users)
### Install from git (recommended)
```bash
# Node 20+ on PATH so the build hook can compile the SPA into the wheel:
uv tool install --with 'keboola-cli[server]' 'git+https://github.com/keboola/cli'
# Run:
kbagent serve --ui
# Open the URL printed at startup -- the browser is auto-authenticated.
```
The wheel ships with the React SPA inside it (`keboola_agent_cli/_ui_dist/`). A hatchling build hook (`hatch_build.py`) runs `npm ci && npm run build` during wheel creation so `uv tool install git+...` produces a self-contained install. If Node is unavailable on the install host the CLI still works but `kbagent serve --ui` will exit with a "no UI bundled" error and instructions to rebuild.
### From a local checkout (editable install)
```bash
uv pip install -e ".[server]"
(cd web/frontend && npm install && npm run build) # produces web/frontend/dist
uv run kbagent serve --ui --port 8001 --config-dir ~/.config/keboola-agent-cli
```
What `--ui` does on top of `kbagent serve`:
1. Mounts `web/frontend/dist` at `/` so the SPA loads from the same port
as the API.
2. Server-renders `index.html` with ``
so the SPA boots already authenticated. No paste step, no Node BFF.
3. Adds a path-rewrite middleware so the SPA's existing `/api/*` calls
reach the bare endpoints (`/projects`, `/configs`, ...).
4. Accepts `?_kbagent_token=...` as a fallback because `EventSource`
(the SSE primitive used on the Jobs page) cannot send custom headers.
`--ui-dist PATH` overrides the dist location; `$KBAGENT_UI_DIST` works
the same way. Both imply `--ui`.
The bearer token is still printed at startup so curl / scripts /
`kbagent http get` keep working alongside the browser session.
## Architecture
```
React SPA (web/frontend, Vite + TanStack Query + Tailwind + Monaco + Mermaid)
|
| Single-process mode: SPA + API on the same FastAPI port,
| token injected into index.html.
|
| Dev mode: SPA via Vite dev server, /api/* proxied to ↓
v
Node BFF (web/backend, Fastify + undici) (DEV / OPTIONAL)
| REST + SSE injects bearer token, no business logic
v
kbagent serve (Python, FastAPI + uvicorn)
|
v
Keboola APIs (Storage, Queue, Manage, AI, ...)
```
The BFF is now optional -- it exists to enable the Vite dev workflow
(hot reload across all three layers) and to host the SPA from a
different process when that's preferred. **Production deployments can
run a single uvicorn process via `--ui`.**
**Building your own client** instead of the bundled SPA? Run plain
`kbagent serve` (no `--ui`). It exposes the same REST + SSE endpoints
and an OpenAPI schema at `/openapi.json`; authenticate with the bearer
token printed at startup or via `$KBAGENT_SERVE_TOKEN`. The
`kbagent http get|post|patch|delete` CLI is a thin wrapper for shell
use.
## Dev mode (three terminals)
Use this when editing React / TypeScript and you want hot reload.
Prerequisite: `uv pip install -e ".[server]"`, Node 20+, and
`(cd web/backend && npm install)` / `(cd web/frontend && npm install)`.
### Terminal 1 -- the kbagent kernel
```bash
uv run kbagent serve --port 8001 --config-dir /tmp/kbagent/.kbagent
# Prints a bearer token on startup -- copy it.
```
### Terminal 2 -- the Node BFF
```bash
cd web/backend
KBAGENT_SERVE_TOKEN= PORT=8000 npm run dev
```
### Terminal 3 -- the React dev server
```bash
cd web/frontend
npm run dev
# Open http://localhost:5173/
```
The Vite dev server proxies `/api/*` and `/__bff/*` to the BFF; the BFF
attaches the bearer token and forwards to `kbagent serve`. Hot reload
works for all three layers.
## Production-only BFF mode (legacy)
If you'd rather keep the BFF in production (e.g. you've got auth /
logging middleware in Fastify you want to preserve), the original flow
still works:
```bash
make web-build # builds the React app into web/frontend/dist
uv run kbagent serve --port 8001 --config-dir ~/.config/keboola-agent-cli &
cd web/backend
STATIC_DIR=../frontend/dist \
KBAGENT_SERVE_TOKEN= \
PORT=8000 npm start
# Open http://localhost:8000/
```
## Architecture notes
- **No business logic in the BFF**: it injects the bearer token and
proxies (REST + SSE) verbatim. If you want a different UI language,
swap the BFF -- the React app talks to it through `/api/*`, the BFF
talks to kbagent serve through OpenAPI.
- **Manage tokens are per-request**: writing operations that need
`KBC_MANAGE_API_TOKEN` (org setup, member invites/removes) prompt
for the token in a modal and pass it as `X-Manage-Token` for that
one request. Never persisted, never logged.
- **SSE streams**: jobs page subscribes to
`/api/jobs/{project}/{id}/stream` for live status + log events.
The BFF passes the chunks through; the React `EventSource` API
receives them directly.
- **SQL workspaces**: Monaco editor + `/api/workspaces/.../query`
with CSV-rendering of statement results.
- **Lineage**: cross-project bucket-sharing graph rendered as a
Mermaid diagram + tabular edge list.
## Repo layout
```
web/
backend/ Node 20 + Fastify (TypeScript)
src/
server.ts entrypoint
proxy.ts REST + SSE proxy with bearer auth injection
config.ts env-driven config
package.json
tsconfig.json
frontend/ React 18 + Vite + Tailwind + TanStack Query
src/
api/client.ts thin fetch wrapper + SSE helper
state.tsx global UI state (selected project, branch, page)
types.ts permissive mirror of kbagent service shapes
layout/ Sidebar, TopBar, StatusBar, Shell
components/ Empty, Table, JsonView, ManageTokenModal
pages/ one file per feature area (20 pages)
App.tsx page router (state-driven)
main.tsx React entry
index.css Tailwind base + NERD theme components
index.html
vite.config.ts
tailwind.config.ts
package.json
tsconfig.json
```