# Libre WebUI Documentation (complete)

> Libre WebUI is a self-hosted, privacy-first AI workspace: chat with local Ollama models or any provider plugin, document chat, artifacts, personas, calendar and automations, and sandboxed Work tasks. Apache-2.0 licensed. This file indexes the complete documentation.

Current release: 0.25.0. Index of these pages: https://docs.librewebui.org/llms.txt

---

# Libre WebUI

URL: https://docs.librewebui.org/

import CodeBlock from '@theme/CodeBlock';

<div className="docs-landing">
  <header>
    <p className="docs-landing__eyebrow">Libre WebUI</p>
    <h1 className="docs-landing__title">Make whatever comes next.</h1>
    <p className="docs-landing__lede">
      {"Run Libre WebUI locally, bring the models you choose, work with documents and media, or give an agent a durable project workspace. Everything opens as a tab in one workspace. Start with the task you want to accomplish."}
    </p>
    <div className="docs-landing__actions">
      <a className="docs-landing__primary" href="/QUICK_START">
        {"Install Libre WebUI"}
      </a>
      <a className="docs-landing__secondary" href="https://demo.librewebui.org">
        {"Try the demo"}
      </a>
      <a
        className="docs-landing__source"
        href="https://github.com/libre-webui/libre-webui"
      >
        {"View source"}
      </a>
    </div>
    <div className="docs-landing__command-block">
      <p className="docs-landing__command-label">One-command local start</p>
      <div className="docs-landing__command">
        <CodeBlock language="bash">npx libre-webui@latest</CodeBlock>
      </div>
      <p className="docs-landing__command-note">
        {"Open your private workspace at "}<code>http://localhost:8080</code>{"."}
      </p>
    </div>
  </header>

  <section className="docs-landing__section" aria-labelledby="choose-a-task">
    <p className="docs-landing__section-label">01 / Choose a task</p>
    <h2 id="choose-a-task">What do you want to do?</h2>

    <div className="docs-landing__grid">
      <article className="docs-landing-card">
        <span className="docs-landing-card__index">01</span>
        <h3 className="docs-landing-card__title">Run privately</h3>
        <p className="docs-landing-card__copy">
          {"Install Libre WebUI on your own machine and begin with Ollama or another model you control."}
        </p>
        <a className="docs-landing-card__link" href="/QUICK_START">
          {"Follow the Quick Start"}
        </a>
      </article>

      <article className="docs-landing-card">
        <span className="docs-landing-card__index">02</span>
        <h3 className="docs-landing-card__title">Connect your models</h3>
        <p className="docs-landing-card__copy">
          {"Use bundled providers, an OpenAI-compatible service, a self-hosted gateway, or a coding agent already installed on the machine."}
        </p>
        <a className="docs-landing-card__link" href="/PROVIDER_CONNECTIONS">
          {"Connect a model provider"}
        </a>
      </article>

      <article className="docs-landing-card">
        <span className="docs-landing-card__index">03</span>
        <h3 className="docs-landing-card__title">Build with Work</h3>
        <p className="docs-landing-card__copy">
          {"Give an agent a durable project with conversation, files, a terminal, tools, and a live application preview."}
        </p>
        <a className="docs-landing-card__link" href="/WORKSPACES">
          {"Explore Libre WebUI Work"}
        </a>
      </article>

      <article className="docs-landing-card">
        <span className="docs-landing-card__index">04</span>
        <h3 className="docs-landing-card__title">Deploy for others</h3>
        <p className="docs-landing-card__copy">
          {"Move from a local workspace to a maintained deployment with persistent data and access controls."}
        </p>
        <a className="docs-landing-card__link" href="/DOCKER">
          {"Plan a deployment"}
        </a>
      </article>
    </div>

  </section>

  <section className="docs-landing__section" aria-labelledby="find-your-way">
    <p className="docs-landing__section-label">02 / Get around</p>
    <h2 id="find-your-way">Everything is one tab away.</h2>

    <div className="docs-landing__path-grid">
      <div className="docs-landing-path">
        <h3>Start from Home</h3>
        <p>{"Home greets you, offers what to start, and lists the chats and Work sessions worth picking back up, with a live badge when a Work runtime is active."}</p>
      </div>

      <div className="docs-landing-path">
        <h3>Keep work side by side</h3>
        <p>{"Chats, Work sessions, and pages open as tabs beside Home. They survive a reload, so returning to a project returns you to where you left it."}</p>
      </div>

      <div className="docs-landing-path">
        <h3>Search instead of hunting</h3>
        <p>{"Press Cmd/Ctrl + K anywhere to reach a chat, a Work session, or an action. It works while you are typing a message, so a draft is never lost to navigation."}</p>
        <ul>
          <li><a href="/KEYBOARD_SHORTCUTS">All keyboard shortcuts</a></li>
        </ul>
      </div>
    </div>

  </section>

  <section className="docs-landing__section" aria-labelledby="read-the-docs">
    <p className="docs-landing__section-label">03 / Browse guides</p>
    <h2 id="read-the-docs">Find the path that matches your work.</h2>

    <div className="docs-landing__path-grid">
      <div className="docs-landing-path">
        <h3>Models and providers</h3>
        <p>{"Choose where inference runs and connect the services that fit your work."}</p>
        <ul>
          <li><a href="/WORKING_WITH_MODELS">Working with Models</a></li>
          <li><a href="/PROVIDER_CONNECTIONS">Connect Model Providers</a></li>
          <li><a href="/AGENT_CLI_MODELS">Use an Installed Coding Agent</a></li>
          <li><a href="/MLX_APPLE_SILICON">MLX LM on Apple Silicon</a></li>
          <li><a href="/HUGGINGFACE_HUB">Hugging Face Hub</a></li>
        </ul>
      </div>

      <div className="docs-landing-path">
        <h3>Create and automate</h3>
        <p>{"Turn conversations into durable projects, reusable context, and working material."}</p>
        <ul>
          <li><a href="/WORKSPACES">Work: Isolated Workspaces</a></li>
          <li><a href="/CALENDAR">Calendar</a></li>
          <li><a href="/AUTOMATIONS">Automations</a></li>
          <li><a href="/RAG_FEATURE">Document Chat</a></li>
          <li><a href="/ARTIFACTS_FEATURE">Artifacts</a></li>
          <li><a href="/PERSONA_DEVELOPMENT_FRAMEWORK">Personas</a></li>
          <li><a href="/KEYBOARD_SHORTCUTS">Keyboard Shortcuts</a></li>
        </ul>
      </div>

      <div className="docs-landing-path">
        <h3>Deploy and administer</h3>
        <p>{"Run a maintained installation with deliberate access, data, and configuration choices."}</p>
        <ul>
          <li><a href="/DOCKER">Docker</a></li>
          <li><a href="/AUTHENTICATION">Authentication</a></li>
          <li><a href="/ENVIRONMENT_VARIABLES">Environment Variables</a></li>
          <li><a href="/TROUBLESHOOTING">Troubleshooting</a></li>
        </ul>
      </div>
    </div>

  </section>
</div>

---

# Quick Start

URL: https://docs.librewebui.org/quick-start

# Quick Start

## Requirements

| Requirement | Minimum  | Recommended                         |
| ----------- | -------- | ----------------------------------- |
| Node.js     | 22.22+   | Latest LTS                          |
| RAM         | 8 GB     | 16 GB+                              |
| Disk        | 5 GB     | 20 GB+ for models                   |
| GPU         | Optional | 8 GB+ VRAM for fast local inference |
| Docker      | Optional | Required for Work tasks             |

Libre WebUI works with CPU-only Ollama, but smaller models are a better fit on CPU. For cloud provider plugins, you only need the relevant API key.

Chat, documents, artifacts, and provider-backed features do not require Docker.
Work does: Docker must be installed on the machine running the Libre WebUI
backend, and the backend process must be allowed to invoke it. The
`npx libre-webui` command does not install Docker. If Docker is unavailable, the
rest of Libre WebUI continues to run while Work reports **Runtime unavailable**;
model commands are never run directly on the host as a fallback.

## Start Libre WebUI

```bash
npx libre-webui@latest
```

Open [http://localhost:8080](http://localhost:8080). Create the first account;
that account becomes the administrator. Later public registration remains
disabled unless you explicitly set `ENABLE_SIGNUP=true`.

The packaged launcher keeps persistent state in `~/.libre-webui`. Set an
absolute `DATA_DIR` to choose another location; relative values are resolved
from the directory where you run `npx`.

## Install Ollama

Install [Ollama](https://ollama.com), then pull a small general model:

```bash
ollama pull gemma3:4b
```

Other good first models are `qwen3:8b`, `deepseek-r1:8b`, and `mistral`. Use the Model Manager in Libre WebUI to browse installed models, search the live Ollama Library, and pull models without leaving the app.

:::tip Embeddings for documents
For semantic document search, also install an embedding model:

```bash
ollama pull nomic-embed-text
```

:::

## Add Cloud Providers

Cloud providers are optional. Add keys per user in **Settings → API keys**, or
server-wide in `backend/.env` (restart the backend afterwards), then enable the
provider in Settings:

```env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GROQ_API_KEY=gsk_...
GEMINI_API_KEY=...
OPENROUTER_API_KEY=sk-or-...
KIMI_API_KEY=...
```

Provider model lists are refreshed by the app when supported. You do not need to keep the docs open to find the latest model names.

## Start Your First Work Task

Work gives each task a persistent conversation and a separate container-backed
filesystem. It is available to authenticated administrators.

1. Install and start Docker on the machine running the Libre WebUI backend.
2. Start a task from the **Work** button in the sidebar header, the tab bar's
   `+` menu, the Home page, or `Cmd/Ctrl + Shift + U`.
3. Choose a model that supports tool calling.
4. Describe what you want to build or change, then select **Run**.
5. Follow the result in the workspace pane's **Files**, **Activity**, **Git**,
   **Terminal**, and **Preview** tabs.

The task remains in the sidebar so you can return to the same conversation and
files later. Stopping a run or preview preserves the workspace. Deleting the
task permanently deletes its workspace.

Work can use an installed Ollama model, an Ollama Cloud model, or a configured
chat or completion-provider plugin. When you select a remote provider, Libre
WebUI shows a disclosure before the run: the provider receives the
conversation, tool definitions, and any tool results requested by the model.
An autonomous run can make multiple paid provider calls.

See [Work: Isolated Workspaces](./WORKSPACES) for the runtime, persistence,
network, preview, and security details.

## Docker

```bash
git clone https://github.com/libre-webui/libre-webui
cd libre-webui
docker compose up -d
```

Create the first administrator in the browser. No registration flag or restart
is required; subsequent public registration is closed by default.

If Ollama is already running on the host or another machine:

```bash
docker compose -f docker-compose.external-ollama.yml up -d
```

For NVIDIA GPU acceleration, use the GPU compose file provided by the repository.

Repository Compose files mount the host Docker socket so Work is available when
Docker is installed. This grants the application root-equivalent control of the
host, so read [Work: Isolated Workspaces](./WORKSPACES) first. On Linux, set
`DOCKER_GID` in `.env` to the group that owns the socket. Remove the mount if
Work is not wanted.

Bundled Ollama is also internal-only. Add
`-f docker-compose.ollama-host.yml` only when another host process needs its API;
the override binds to loopback by default.

## Find Your Way Around

The **Home** tab has three sections: **Start** (new chat, incognito chat, new
Work task), **Continue** (your recent conversations and tasks), and **Explore**
(Notes, Calendar, Automations, Models, Personas, Imagine). The same
destinations sit in the sidebar's navigation row and in the command palette
(`Cmd/Ctrl + K`), which fuzzy-searches your chats, Work tasks, and app actions.

- [Calendar](./CALENDAR) — a per-user calendar with recurring events.
- [Automations](./AUTOMATIONS) — scheduled AI runs delivered as chat sessions.

## Keyboard Shortcuts

| Shortcut               | Action                               |
| ---------------------- | ------------------------------------ |
| `Cmd/Ctrl + K`         | Command palette (works while typing) |
| `Cmd/Ctrl + Shift + O` | New chat                             |
| `Cmd/Ctrl + Shift + U` | New Work task                        |
| `Cmd/Ctrl + B`         | Toggle sidebar                       |
| `Cmd/Ctrl + ,`         | Settings                             |
| `Cmd/Ctrl + D`         | Toggle theme                         |
| `?`                    | Open Settings on the Shortcuts tab   |
| `Esc`                  | Close the settings modal             |
| `Enter`                | Send message                         |
| `Shift + Enter`        | New line                             |

## Next Steps

- [Working with Models](./WORKING_WITH_MODELS)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Calendar](./CALENDAR)
- [Automations](./AUTOMATIONS)
- [Hardware Requirements](./HARDWARE_REQUIREMENTS)
- [Document Chat](./RAG_FEATURE)
- [Artifacts](./ARTIFACTS_FEATURE)
- [Docker](./DOCKER)

---

# Working with AI Models

URL: https://docs.librewebui.org/working-with-models

# Working with AI Models

Libre WebUI can use local Ollama models and plugin-backed cloud models in the same workspace. The Model Manager shows installed Ollama models, running models, live Ollama Library results, Hugging Face GGUF entries, and Ollama Cloud entries where available.

## Choosing a First Model

Use these as starting points, then switch based on your hardware and task:

| Model              | Good for                               | Typical fit                 |
| ------------------ | -------------------------------------- | --------------------------- |
| `gemma3:4b`        | Fast general chat                      | 8 GB RAM or entry GPU       |
| `qwen3:8b`         | General chat, coding, multilingual use | 16 GB RAM or 6-8 GB VRAM    |
| `deepseek-r1:8b`   | Reasoning-heavy prompts                | 16 GB RAM or 6-8 GB VRAM    |
| `mistral`          | Balanced general assistant work        | 8-16 GB RAM                 |
| `nomic-embed-text` | Document embeddings                    | Small local embedding model |

Large models such as 30B, 70B, and MoE models can be excellent, but they need much more memory. If you are not sure, start small and move up after the model is working smoothly.

## Model Manager

Open **Models** from the sidebar to:

- Pull models from Ollama by name.
- Search the live Ollama Library instead of relying on a static list.
- View installed and running models.
- Stop or unload running models.
- Delete models you no longer need.
- Pull Hugging Face GGUF models through Ollama when compatible.
- Pull Ollama Cloud models from the cloud filter.

For Ollama Cloud results, the UI normalizes cloud model names before pulling. If a cloud model requires the `:cloud` or `-cloud` suffix, Libre WebUI applies that for you from the cloud model flow.

## Model Catalog and Visibility

The **Model Catalog** at the top of the Models page lists every chat model you can pick, local and provider-backed alike, with a provider badge and a search box. Use **Make default** on any row to set the model new chats start with.

Administrators get one more control per row: an eye toggle that hides a model from everyone else's model pickers. Hiding trims long catalogs down to the models a server actually wants people using — it is a listing refinement, not an authorization gate, so treat it as curation rather than a security boundary. Administrators always see the full list, with hidden models marked.

## Local vs Cloud Models

| Mode             | Strengths                                              | Tradeoffs                                                     |
| ---------------- | ------------------------------------------------------ | ------------------------------------------------------------- |
| Local Ollama     | Private, offline after download, predictable cost      | Depends on your CPU/GPU/RAM                                   |
| Ollama Cloud     | Familiar Ollama workflow without local hardware limits | Requires cloud access and network                             |
| Provider plugins | Access to managed models from multiple providers       | API keys, provider pricing, and provider privacy policy apply |

You can keep local models for private work and enable provider plugins for tasks that need larger hosted models.

## Default Vision Model

You can chat with a fast text model and still send images. Pick a vision model
under **Settings → Model → Specialized Models → Vision Model**; whenever the
outgoing chat context contains images — a new attachment, an image earlier in
the session, or history in an incognito chat — that turn is routed to the
configured vision model instead of the session model. Text-only turns keep the
session model.

The setting is per user, and the routing is automatic and silent. Leaving the
selection on **Use the current chat model** disables it. Note that the check is
for images, not for the session model's abilities: when a vision model is
configured, every image-bearing turn uses it, even if the session model could
handle images itself.

The selection stores the exact provider identity (Ollama or a specific plugin)
together with the model name, so a provider cannot capture an identically named
model. If the saved selection loses that identity — for example the model or
provider is no longer available — an image-bearing turn fails with:

> The configured vision model has no provider identity. Re-select it in
> Settings > Model > Vision Model.

Re-selecting the model in Settings repairs it. Failing loudly is deliberate;
Libre WebUI does not silently substitute another provider.

## Models for Work

Work needs a chat model that can call tools. It can use:

- An installed Ollama model that advertises the `tools` capability.
- An Ollama Cloud model available through the configured Ollama endpoint.
- A model listed by an active chat or completion plugin with credentials
  configured for the current administrator.

Plugin-backed Work runs use the provider adapter appropriate to the configured
plugin: OpenAI-compatible, Anthropic, or Gemini. Libre WebUI persists the exact
provider type and plugin identifier with the task and each run, so a plugin
cannot capture an identically named Ollama model. If the selected model or
provider rejects tool calling, the run fails instead of silently switching to
another provider.

Local Ollama keeps model requests on the configured Ollama infrastructure.
With a remote model, the configured provider receives the Work system prompt,
conversation, tool definitions, and tool results. Tool results can include
source text, command output, or directory listings requested by the model.
Workspace volumes and provider credentials remain on the backend host, but a
file's contents can leave that host when they are included in a tool result.

One autonomous Work run can make multiple model calls. Check the remote
provider's pricing, retention, and training policies before using sensitive
projects. Libre WebUI shows a remote-provider notice in Work with a per-user
dismiss control.

## Hardware Guide

| System                              | Practical model range | Notes                           |
| ----------------------------------- | --------------------- | ------------------------------- |
| CPU only, 8-16 GB RAM               | 1B-4B                 | Good for light chat and testing |
| 8 GB VRAM                           | 4B-8B quantized       | Comfortable starting point      |
| 12-16 GB VRAM                       | 8B-14B quantized      | Good daily driver range         |
| 24 GB VRAM                          | 14B-32B quantized     | Strong local workstation        |
| 48 GB+ VRAM or large unified memory | 32B-70B quantized     | Large model experimentation     |

Quantized models use less memory. Q4 quantizations are usually the practical default; Q8 uses more memory for better quality.

## Task-Based Recommendations

| Task            | Model direction                                                     |
| --------------- | ------------------------------------------------------------------- |
| Fast chat       | `gemma3:4b`, `mistral`, small Qwen models                           |
| Coding          | Qwen Coder, DeepSeek Coder, Codestral, provider coding models       |
| Reasoning       | DeepSeek-R1 family, larger Qwen models, provider reasoning models   |
| Vision          | Multimodal models such as LLaVA, Qwen VL, or provider vision models |
| Document search | `nomic-embed-text` or another embedding model                       |
| Text-to-speech  | TTS plugins such as Qwen3-TTS or Kyutai TTS                         |

Provider model names change frequently. In Libre WebUI, use the provider’s model discovery where available, or paste the exact model ID from the provider dashboard.

## Prompting and Settings

- Generation controls such as temperature, token limits, context length, and
  penalties are grouped under **Advanced generation settings** and remain
  closed by default.
- Lower temperature (`0.1-0.3`) for factual, repeatable answers.
- Medium temperature (`0.5-0.7`) for normal assistant work.
- Higher temperature (`0.8+`) for brainstorming and creative writing.
- Keep context length reasonable when you are close to memory limits.
- Use personas when you want persistent model parameters and a reusable system prompt.

## Troubleshooting

**Pull fails**

- Confirm Ollama is running: `ollama list`.
- Try the same pull in a terminal to see Ollama’s raw error.
- Check disk space before pulling large models.
- If you are using the Model Manager cloud filter, let Libre WebUI handle cloud suffixes.

**Responses are slow**

- Try a smaller model or a lower quantization.
- Check `ollama ps` to see what is loaded.
- Close other GPU-heavy apps.
- Reduce context length.

**Out of memory**

- Move from Q8 to Q4.
- Use an 8B model instead of a 14B model.
- Keep only the model you need loaded.
- On Docker, confirm the container can reach the GPU or the host Ollama instance.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Hardware Requirements](./HARDWARE_REQUIREMENTS)
- [Plugin Architecture](./PLUGIN_ARCHITECTURE)
- [Hugging Face Hub](./HUGGINGFACE_HUB)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Pro Tips

URL: https://docs.librewebui.org/pro-tips

# Pro Tips

This page collects practical workflows that make Libre WebUI feel faster, cleaner, and more reliable in daily use.

## Keep a Small Daily Model Loaded

Use a fast local model for routine work and switch to larger models only when the task needs it.

Good daily-driver examples:

- `gemma3:4b` for fast everyday chat
- `qwen3:8b` for stronger general work
- `deepseek-r1:8b` for reasoning prompts
- `nomic-embed-text` for document embeddings

Open **Models** to see which models are running. Unload models you are not using when VRAM gets tight.

## Use Incognito Chat for Conversations That Should Not Persist

Start an incognito chat from the tab bar's `+` menu, the command palette, the
Home page, or the ghost button on the chat welcome screen. It also has a
direct URL: `/chat?incognito=1`.

An incognito chat is never persisted: no session is created on the server, no
message is saved, and it never appears in the sidebar or history. The chat
shows a **Private Mode** banner ("This conversation won't be saved"). Opening
a saved chat leaves incognito mode; reloading an incognito tab starts a fresh
empty private chat, so the previous turns are gone.

Be clear about the boundary: incognito controls persistence, not provider
exposure. The selected model — local or remote — still receives the full
conversation, and document context still applies when it is enabled. For a
conversation that must not leave your infrastructure, combine incognito with a
local Ollama model.

## Manage Tabs with the Context Menu

Right-click a tab (or press `Shift + F10` on a focused tab) for:

- **Close tab**
- **Close other tabs**
- **Close tabs to the right**
- **Close all tabs**

Home is always the first tab and cannot be closed. Administrators also get
direct **User Management**, **System**, and **Provider Usage** entries in the
`+` menu — and can pin any of the three into the sidebar footer (next to
Settings) with the pin icon in the avatar menu, so they never need the menu
again.

## Fly Around with the Command Palette

`Cmd/Ctrl + K` opens the command palette from anywhere — including while the
composer has focus. It fuzzy-matches across app actions, your chats, and your
Work tasks, so partial or misspelled queries still land: `autmtn` finds
**Automations**, "pictures" finds **Imagine**, "dark" finds the theme toggle.
Matched characters are highlighted, results are ranked by relevance, and with
no query you get your most recent chats and tasks. Navigate with `↑`/`↓`,
open with `Enter`, close with `Esc` (or `Cmd/Ctrl + K` again).

## Theme Default

New installs use the dark theme, applied before first paint so there is no
light flash. A saved light preference is always respected. Libre WebUI does
not follow the operating system's theme setting; switch explicitly with
`Cmd/Ctrl + D` or from Settings.

## Keep Work Tasks Focused

Use a separate Work task for each project or independent goal. Every task has
its own conversation, managed container identity, and persistent files. The
container itself can stop or be recreated while its named volume survives, so
reusing the same task preserves useful context while starting a new task creates
a clean boundary.

A good first instruction gives the model:

- The result you want.
- Important technical or design constraints.
- The command or behavior that should verify completion.
- Any files or interfaces that must remain unchanged.

Follow progress in **Activity**, then inspect and test the result in **Files**,
**Git**, **Terminal**, and **Preview**. The file editor supports syntax highlighting in light and dark
themes, browser-backed unsaved drafts, and formatting for supported file types.
Use `Cmd/Ctrl + S` to save and `Shift + Alt + F` to format.

Use an installed tool-capable Ollama model when you want model traffic to stay
on your configured Ollama infrastructure. A remote or cloud model can reduce
local inference memory pressure, but it can make multiple billable calls and
receives requested tool results, which may contain workspace data.

Stopping a run or preview keeps the workspace. Deleting a Work task removes its
workspace permanently, so copy out anything you need first.

## Use Personas for Repeatable Work

Create personas for workflows you repeat:

- A concise code reviewer with low temperature.
- A writing editor with a clear style guide.
- A research assistant with document search enabled.
- A support assistant with a fixed tone and response structure.

Personas store the selected model, system prompt, generation parameters, avatar/background, and optional memory/mutation settings. They can also be exported and imported as JSON.

## Keep Durable Notes Beside Your Work

Open **Notes** from the create menu when information should remain independent
of one chat or Work task. Notes support Markdown preview, explicit editing,
search, and automatic saving. They are account-scoped and are included in a
full user archive.

## Make Artifacts More Reliable

Libre WebUI detects explicit artifact tags, fenced code blocks, standalone HTML documents, and common multi-file HTML bundles. To get the best artifact output from a model, ask for:

```text
Create one complete self-contained HTML file.
Inline the CSS and JavaScript.
Do not rely on external files unless they are CDN URLs.
```

If you want separate blocks, name them clearly:

````markdown
```html filename="index.html"
...
```

```css filename="style.css"
...
```

```js filename="app.js"
...
```
````

Libre WebUI will try to bundle local CSS and JavaScript blocks into the HTML preview.

## Use Document Chat Deliberately

Document Chat currently accepts PDF and plain-text files up to 10 MB. Search works in two modes:

- Keyword search is always available.
- Semantic search is used when embeddings are enabled in Settings and an embedding model is available.

Install `nomic-embed-text` if you want an easy local embedding model:

```bash
ollama pull nomic-embed-text
```

For best results, upload focused documents per chat instead of one huge mixed document set.

## Tune Generation Settings

| Setting        | Practical use                                                       |
| -------------- | ------------------------------------------------------------------- |
| Temperature    | Lower for accuracy, higher for creative exploration                 |
| Top P / Top K  | Leave defaults unless you are deliberately tuning sampling          |
| Context window | Increase for long chats only if your model and memory can handle it |
| Max tokens     | Limit long answers or raise for code/artifact generation            |
| Repeat penalty | Raise slightly when a model loops                                   |

When a model behaves badly, first lower temperature, then reduce context pressure, then try another model.

## Decide How Hard a Model Thinks

The control beside the model name in the composer opens the reasoning levels:
**off**, **on**, **low**, **medium**, and **high**. The choice belongs to the
conversation, so it survives a reload and applies to a regenerate; Settings >
Generation holds the default for new replies, and the chat controls panel shows
the same value.

Leave it unset and nothing is sent, which is what every release before this one
did. Set it and the server translates the one value for whichever provider
answers: Ollama takes it in the request body, OpenAI-style providers take a
reasoning effort, and Anthropic and Gemini take a token budget with room
reserved for the answer.

Two things are worth knowing. A model Ollama reports as unable to reason never
receives the setting at all, so the control is simply absent for it. And the
named levels only exist on the models that publish them, such as gpt-oss; on a
model that reasons without levels, a named level simply behaves as **on**, so a
chat that moves between models never errors over it. When a global or pinned
default is set, the composer button shows the level the next reply actually
runs with, and the "Default" entry names what it currently resolves to.

## Watch the Context Window

The ring beside the model name fills as the conversation grows. Hover it for how
full the window is, the tokens used, and the window they run against. It turns
amber past four fifths and red at the window; a model whose window is unknown
shows a dashed ring rather than an empty one.

The count covers what the next request will actually send — compacted history
and abandoned branches cost nothing. It anchors to what the provider measured
for the last reply when it reported one, plus an estimate at four characters
per token for what the conversation added since, marked with a `~` when no
measurement exists yet. A window capped below what the model was trained for
says so: the meter
measures the window the request actually runs with, which is
`OLLAMA_MAX_CONTEXT` (32,768 by default) rather than the model's full trained
length. Raise that variable and both the real window and the meter follow.

Provider models show a window only when their model listing publishes one. When
it does not, the meter still counts the tokens and simply has nothing to divide
them by.

## Let Long Chats Compact Themselves

Administrators can turn on **context compaction** in Settings > Generation. Once a conversation's estimated context passes the token threshold, the server asks a model to summarize the older messages and keeps only the most recent ones verbatim. The summary appears as a conversation-summary card at the point in the chat where the history was folded, and the summarized messages render dimmed: still readable, no longer sent to the model. With compaction on, the "recent messages kept" count is also the rolling window a conversation sends, so raising it genuinely widens what the model sees.

| Setting               | What it controls                                                       |
| --------------------- | ---------------------------------------------------------------------- |
| Token threshold       | Estimated context size that triggers a compaction                      |
| Recent messages kept  | How many of the latest messages always stay verbatim                   |
| Compaction model      | Which model writes summaries; defaults to the conversation's own model |
| Custom summary prompt | Your own instructions, with `{{PREVIOUS_SUMMARY}}` and `{{MESSAGES}}`  |

Compaction is off by default and applies to every user on the server, but each chat keeps a say: the chat controls panel can switch compaction off for one conversation, and every summary card carries an undo — restoring reactivates exactly the messages that summary replaced, one compaction at a time. It never splits a turn: the messages kept verbatim always start on one of your own. Each new compaction folds the previous summary into the new one, so a conversation carries a single running summary. If the summarizer fails, generation continues with the uncompacted history rather than blocking on it.

## Keep Provider Keys Per User

Provider plugins can read environment keys, but user-level credentials are usually cleaner for shared installs. Add keys in Settings so each user controls their own provider access.

Use backend environment variables for deployment-wide defaults or automated installs.

## Make Remote Access Predictable

For phone or LAN access, bind the dev server to the network interface:

```bash
npm run dev:host
```

Then open the machine’s LAN or Tailscale IP **on port 8080** from the other device (`dev:host` serves the frontend on 8080, not Vite's default). In production, set `CORS_ORIGIN` and the frontend API URL explicitly so browsers do not fall back to localhost.

## Check Your Version Without Leaving the App

**Settings → About** compares your build against the latest GitHub release: it
tells you when you're current, links the release page when you're behind, and
says so when a `-dev` build is running ahead of the pinned release. The same
line has a **View changelog** button that reopens the release notes you saw
after upgrading — and if Libre WebUI is useful to you, the **Star on GitHub**
link there is the easiest way to help others find it.

## Keep Docs and UI in Sync

The product changes quickly. Prefer durable docs that describe behavior and workflows, and let the UI show live model lists from providers. Avoid copying long provider catalogs into docs unless the list is generated by the app.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Working with Models](./WORKING_WITH_MODELS)
- [Calendar](./CALENDAR)
- [Automations](./AUTOMATIONS)
- [Document Chat](./RAG_FEATURE)
- [Artifacts](./ARTIFACTS_FEATURE)
- [Personas](./PERSONA_DEVELOPMENT_FRAMEWORK)
- [Environment Variables](./ENVIRONMENT_VARIABLES)

---

# Keyboard Shortcuts

URL: https://docs.librewebui.org/keyboard-shortcuts

# Keyboard Shortcuts

Libre WebUI includes app-level shortcuts for navigation and message composition.

## Global Shortcuts

| Shortcut               | Action                                            |
| ---------------------- | ------------------------------------------------- |
| `Cmd/Ctrl + K`         | Open or close the command palette                 |
| `Cmd/Ctrl + Shift + O` | Start a new chat in a tab                         |
| `Cmd/Ctrl + Shift + U` | Start a new Work task in a tab (with Work access) |
| `Cmd/Ctrl + B`         | Toggle the sidebar                                |
| `Cmd/Ctrl + ,`         | Open Settings                                     |
| `Cmd/Ctrl + D`         | Toggle light/dark theme                           |
| `?` or `Shift + /`     | Open Settings on the Shortcuts tab                |
| `Esc`                  | Close the settings modal                          |

Global shortcuts never fire while an input has focus, and dialogs such as the
command palette, artifact panel, image lightbox, and model selector each close
with their own `Esc`. `Cmd/Ctrl + K` is the exception to the focus rule: it
also works while a message composer has focus, so you can jump somewhere else
without clearing what you were typing.

### Command Palette

The palette fuzzy-searches your chats, Work tasks, and app actions, highlights
the matched characters, and ranks results by relevance.

| Shortcut      | Action                   |
| ------------- | ------------------------ |
| `Up` / `Down` | Move the selection       |
| `Enter`       | Open the selected result |
| `Esc`         | Close the palette        |

## Chat Input

| Shortcut        | Action            |
| --------------- | ----------------- |
| `Enter`         | Send the message  |
| `Shift + Enter` | Insert a new line |

## Editing a Sent Message

While editing one of your own messages:

| Shortcut | Action                                      |
| -------- | ------------------------------------------- |
| `Enter`  | Save the edit and resubmit the conversation |
| `Esc`    | Cancel editing                              |

## Artifact Viewer

The artifact pane uses normal browser controls:

- Drag the panel edge to resize.
- Use Preview and Code tabs to switch views.
- Copy, download, open, or expand artifacts from the toolbar.
- In interactive HTML artifacts, click inside the preview first when the artifact needs keyboard focus.

## Work

### Composer

| Shortcut        | Action             |
| --------------- | ------------------ |
| `Enter`         | Start the Work run |
| `Shift + Enter` | Insert a new line  |

### File Editor

With the code editor focused:

| Shortcut          | Action                                     |
| ----------------- | ------------------------------------------ |
| `Cmd/Ctrl + S`    | Save the open workspace file               |
| `Shift + Alt + F` | Format a supported file within size limits |

### Conversation and Workspace Split

On desktop, focus the divider between the conversation and workspace panes:

| Shortcut               | Action                                          |
| ---------------------- | ----------------------------------------------- |
| `Left` / `Right Arrow` | Resize the conversation by 2 percentage points  |
| `Shift + Left/Right`   | Resize the conversation by 10 percentage points |
| `Home`                 | Use the smallest allowed conversation pane      |
| `End`                  | Use the largest allowed conversation pane       |
| `Enter`                | Reset the split to its default                  |

You can also drag the divider or double-click it to reset. The selected split
is remembered for the current user. Arrow behavior follows the visual direction
of the interface, including Arabic and other right-to-left layouts.

### Workspace Tabs

With one of the **Files**, **Activity**, **Git**, **Terminal**, or **Preview**
tabs focused:

| Shortcut               | Action                                                       |
| ---------------------- | ------------------------------------------------------------ |
| `Left` / `Right Arrow` | Select the adjacent tab in visual direction, wrapping around |
| `Home`                 | Select Files                                                 |
| `End`                  | Select Preview                                               |

## Notes

Browser, OS, and input method shortcuts can take precedence over app shortcuts. If a shortcut does not fire, check whether the browser already owns that key combination.

---

# Demo Mode

URL: https://docs.librewebui.org/demo-mode

# Demo Mode

Demo mode lets public preview deployments show Libre WebUI without requiring a real backend account or provider credentials.

## When Demo Mode Turns On

Demo mode is enabled when:

- `VITE_DEMO_MODE=true`, or
- the frontend is running on a recognized preview host such as Vercel, Netlify, GitHub Pages, or a `demo.`/`preview.` subdomain.

Local development is not demo mode unless `VITE_DEMO_MODE=true` is set.

## Login Behavior

In demo mode, the login form is prefilled with demo credentials and the fields are disabled. Users only click **Sign in**. Signup and OAuth buttons are hidden in demo mode.

## What Demo Mode Provides

Demo mode uses mock API responses for the main product flows:

- Chat sessions and messages
- Models
- Personas
- Artifacts
- Preferences
- Plugin lists

This keeps the demo interactive without writing real user data.

## What Demo Mode Does Not Do

Demo mode is not a production authentication strategy. It does not provide real persistence, real provider calls, real account management, or real Work task containers and command execution.

For a real deployment, run the backend with normal authentication and configure the providers you want to expose.

## Configure Demo Mode

Frontend `.env`:

```env
VITE_DEMO_MODE=true
```

Build and run the frontend as usual. If the backend is also running, demo-mode API mocks still take precedence for supported UI flows.

## Related Docs

- [Authentication](./AUTHENTICATION)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Quick Start](./QUICK_START)

---

# Troubleshooting

URL: https://docs.librewebui.org/troubleshooting

# Troubleshooting

Start with the layer that is failing: browser, frontend, backend, Ollama, provider plugin, or deployment networking.

## Quick Checks

```bash
# App branch and local changes
git status

# Backend process liveness
curl http://localhost:3001/health/live

# Backend dependency readiness (SQLite, schema, and writable data storage)
curl http://localhost:3001/health/ready

# Ollama health
curl http://localhost:11434/api/tags

# Installed Ollama models
ollama list
```

In development, the frontend usually runs on `http://localhost:5173` and the backend on `http://localhost:3001`. The packaged `npx libre-webui` flow serves the app on `http://localhost:8080`.

## Libre WebUI Does Not Start

**Check Node and dependencies**

```bash
node --version
npm install
npm run dev
```

Node.js 22.22 or newer is required.

**Port already in use**

```bash
lsof -i :3001
lsof -i :5173
lsof -i :8080
```

Stop the old process or configure another port.

**Backend cannot write data**

The backend stores data under `DATA_DIR` when set, otherwise under
`backend/data`. Source launches resolve a relative `DATA_DIR` from the backend
directory, not the shell's current directory. Thus `DATA_DIR=./data` selects
`backend/data`, while the historically supported `DATA_DIR=./backend/data`
selects `backend/backend/data`. Make sure the selected directory is writable.
With `DATA_DIR` unset, Libre preserves the historical directory when it is the
only existing store. If both locations contain data, stop Libre, back up both,
and choose or migrate deliberately; Libre never merges or copies divergent
databases.

The health endpoints deliberately distinguish a running process from a usable
application:

- `/health` and `/health/live` return `200` while the backend process can serve
  HTTP. Optional model providers do not affect liveness.
- `/health/ready` returns `503` when a required database, schema, storage, or
  registered platform dependency is unavailable. It does not wait for optional
  model providers. Its public response omits error messages and internal
  details.
- `/health/deep` performs SQLite integrity and foreign-key checks in a bounded
  worker and aggregates optional server-level provider probes such as Ollama.
  An optional provider outage appears as a warning and does not make required
  dependencies unready. The endpoint requires a current administrator bearer
  token and is not suitable for a frequent orchestrator probe.

```bash
curl -H "Authorization: Bearer $LIBRE_ADMIN_TOKEN" \
  http://localhost:3001/health/deep
```

## Browser Cannot Reach the Backend

For local development, the frontend uses `VITE_API_BASE_URL` when set and otherwise falls back to the development backend.

Frontend `.env` example:

```env
VITE_API_BASE_URL=http://localhost:3001/api
VITE_WS_BASE_URL=ws://localhost:3001
```

`VITE_WS_BASE_URL` is optional, but when set it is the shared base for Chat and
Work terminal sockets. Use an absolute `ws:` or `wss:` URL; a path prefix such
as `wss://example.com/libre` is supported. Do not include credentials, query
parameters, or fragments. Restart/rebuild the frontend after changing a Vite
variable.

Backend `.env` example:

```env
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
```

For phone, LAN, or Tailscale access, do not point the phone browser at `localhost`; use the laptop’s LAN or Tailscale IP and run the dev server with host binding:

```bash
npm run dev:host
```

## Chat Doesn't Stream Behind a Reverse Proxy

The typical symptom is that messages send but no reply ever renders, while the
browser console shows a WebSocket connection failure. Confirm that the proxy
allows WebSocket upgrades and does not close long-lived connections.

When either value is configured, browser upgrades that send an `Origin` header
are checked against `CORS_ORIGIN` and `BASE_URL`. Set at least one for a remote
deployment; with neither configured, the Origin filter remains permissive for
local development. Electron and other non-browser clients may omit `Origin`,
but they still must first exchange their Authorization header for a short-
lived, one-use ticket. Keep the backend behind TLS and the same network or
reverse-proxy access controls used for the HTTP API.

For a public hostname, allow that browser origin in the Libre WebUI service:

```yaml
services:
  libre-webui:
    environment:
      CORS_ORIGIN: https://chat.example.com
      BASE_URL: https://chat.example.com
```

The nginx and Caddy examples below assume the proxy runs on the Docker host,
where the repository's Compose setup publishes Libre WebUI on port `8080`. If
the proxy joins the Compose network instead, use `libre-webui:3001` as the
upstream address.

### nginx

nginx requires the upgrade headers to be forwarded explicitly. The longer read
timeout keeps an otherwise idle chat connection open while the model works.

```nginx
location /ws {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
}
```

Reload nginx after validating the configuration with `nginx -t`.

### Caddy

Caddy's `reverse_proxy` supports WebSockets out of the box, so no upgrade
headers are needed:

```caddyfile
chat.example.com {
    reverse_proxy 127.0.0.1:8080
}
```

### Traefik

Traefik also handles WebSocket upgrades by default. When its Docker provider
shares Libre WebUI's network, only the normal router and service labels are
needed, for example:

```yaml
labels:
  - 'traefik.enable=true'
  - 'traefik.http.routers.libre-webui.rule=Host(`chat.example.com`)'
  - 'traefik.http.routers.libre-webui.entrypoints=websecure'
  - 'traefik.http.routers.libre-webui.tls=true'
  - 'traefik.http.services.libre-webui.loadbalancer.server.port=3001'
```

If streams connect but drop later, check the idle timeout on any proxy or load
balancer in front of Traefik. When Traefik itself is enforcing the limit,
adjust the entry point's `transport.respondingTimeouts` setting.

## Ollama Is Not Detected

**Confirm Ollama is running**

```bash
curl http://localhost:11434/api/tags
```

**Configure a custom Ollama URL**

Backend `.env`:

```env
OLLAMA_BASE_URL=http://localhost:11434
```

If Libre WebUI runs in Docker and Ollama runs on the host, use the external Ollama compose file or point `OLLAMA_BASE_URL` at the host address reachable from the container.

## Model Pull Problems

**Pull from terminal first**

```bash
ollama pull gemma3:4b
```

If the terminal pull fails, the problem is outside Libre WebUI.

**Cloud models**

Use the cloud filter in the Model Manager for Ollama Cloud models. Libre WebUI normalizes required cloud suffixes from that flow, so users should not need to manually add `:cloud` for supported cloud entries.

**User cannot pull models**

Administrators can disable model pulls for normal users. Check admin settings if a non-admin user can browse models but cannot install them.

## Chat Is Slow or Fails

- Use a smaller model.
- Check loaded models with `ollama ps`.
- Reduce context length.
- Reduce max tokens for very long responses.
- Confirm the model fits in RAM/VRAM.
- For provider plugins, confirm the API key and provider quota.

## OpenAI Image Generation Is Unavailable

- Activate the bundled OpenAI provider. Save an API key for the current user,
  or configure the trusted bundled provider's `OPENAI_API_KEY` environment
  fallback.
- Open Image Generation settings, enable image generation, and select one of
  the advertised GPT Image models.
- Prefer `gpt-image-2`. The older GPT Image IDs remain available only for
  compatibility with existing configurations and are deprecated upstream.
- Leave the OpenAI `image_endpoint` override blank unless you operate a
  compatible image endpoint. A Chat `/responses` or `/chat/completions`
  endpoint cannot process Image API requests.
- If OpenAI rejects a GPT Image request despite a valid key and quota, confirm
  that the API organization is eligible to use GPT Image models.

Image availability is evaluated with the current user's saved credential or the
trusted bundled provider's environment fallback. A key saved only in another
user's settings does not expose image models.

## Provider Endpoint Problems

If an OpenAI-compatible provider receives requests at the wrong path, check its
settings under **Settings → Plugins**:

- Choose **Chat Completions** for `/chat/completions` payloads or **Responses**
  for `/responses` payloads.
- Enter the API root, such as `https://provider.example/v1`, as the base URL.
- Leave API path empty for the mode's default, or enter a leading-slash path
  supplied by the provider.
- A genuinely custom legacy full endpoint intentionally has highest
  precedence, so clear it when switching back to Base URL and API Path. Stored
  values that merely equal the bundled manifest's old default are ignored
  automatically after an upgrade. When a custom endpoint ends in
  `/chat/completions` or `/responses`, that suffix also determines the request
  format so an override cannot receive the wrong payload.

Imported plugin JSON supports providers that use an OpenAI Chat Completions,
OpenAI Responses, Anthropic, or Gemini-compatible wire format. If the provider
uses a proprietary payload, streaming event, tool-call, or response format, it
needs a backend adapter; changing only the endpoint cannot translate it.

Provider URLs may use HTTP or HTTPS. HTTP sends credentials and provider traffic
without transport encryption, so reserve it for a self-hosted gateway on a
trusted network and prefer HTTPS whenever TLS is available. Base URLs cannot
contain query strings or fragments, and relative API paths cannot contain
literal or repeatedly encoded traversal segments, query strings, or fragments.
Excessive encoding is rejected when it does not stabilize within the validation
bound.

Model refresh replaces known operation suffixes, including `/responses`, with
`/models`. Activation, explicit refresh, and saved connection overrides use the
current user's endpoint and API key. Saving or removing that user's API key and
resetting connection overrides also refresh the list; unrelated generation
parameters do not. Discovered IDs are stored per user and never overwrite the
shared plugin JSON. If the derived route is not supported by the provider,
configure model IDs manually in the plugin's `model_map`.

Provider requests intentionally do not follow HTTP redirects, including model
discovery, Chat, Work, image generation, embeddings, and text-to-speech.
Configure the final destination URL rather than a redirecting URL. This
fail-closed behavior keeps an authorization header from hopping to a destination
that was not validated.

If Work reports that provider routing changed during a run, start a new run
after finishing the provider settings update. Work intentionally stops before
its next provider request so prior tool state cannot be replayed to a different
mode, endpoint, or API-key authentication boundary.

Requests originate from the backend, so `localhost` refers to the Libre WebUI
container when the backend runs in a container, not automatically to the host
machine. For a Compose or Kubernetes deployment, use the gateway's service DNS
name, for example `http://ai-gateway:8080/v1`. Use
`http://host.docker.internal:8080/v1` only when the container runtime exposes
that host alias. HTTP traffic is plaintext even when the name resolves
privately.

Image model availability, endpoint overrides, and API keys are resolved for
the current user too. If an image request appears to use another account's
provider settings, verify that the request is authenticated as the expected
user.

The following security and ownership rules also apply:

- Sign in as an administrator to change provider routing. Plugin definitions
  and connection fields are instance-managed configuration; normal users can
  still save generation settings, credentials, and their own activation state.
- When using the legacy `endpoint` or `api_url` override, enter the full API
  endpoint URL, including the operation path (for example,
  `https://provider.example/v1/chat/completions`). Enter an API root only in
  `base_url`, paired with `api_mode` and an optional `api_path`.
- Absolute HTTP and HTTPS endpoint URLs are accepted. Use HTTP only for a
  self-hosted gateway on a trusted network because API keys, prompts, and
  responses are otherwise sent without transport encryption.
- An empty override uses the endpoint bundled in the plugin definition. An
  explicit malformed or unsafe override is rejected; Libre WebUI does not
  silently send that request to the bundled provider endpoint.
- A deployment environment key is used only when an unshadowed bundled
  definition retains its trusted root endpoint, authentication fields,
  capability endpoints and selectors, and routing-variable defaults. Imported
  definitions, writable definitions that reuse a bundled ID, and
  administrator-saved custom routes require a credential saved by the same
  account. Libre WebUI
  intentionally reports the provider as unavailable and skips discovery if
  only the environment key exists.
- Pre-upgrade custom definitions are quarantined because older releases did not
  record administrator provenance. Re-import the JSON as an administrator,
  then have each user activate it again. Editing an approved plugin JSON
  directly quarantines it again; use the administrator install or update flow
  so its source path and definition hash are recorded.
- Saved credentials are bound to the route, authentication contract,
  definition, and source in effect when they were entered. After changing an
  endpoint or definition, save that account's credential again. An old unbound
  credential migrates automatically only on an exact anchored bundled route.
- Imported plugins may use `api_url` as a legacy full-operation URL alias.
  `endpoint` wins when both fields are set. If model discovery lives elsewhere,
  set the complete model-list URL in `models_endpoint`; it is validated and
  redirects are not followed.
- Activate the plugin after saving its endpoint and credential. Activation
  derives a `/models` URL from the saved full endpoint and uses the activating
  user's credential for discovery unless `models_endpoint` is set. Saving or
  resetting any of these connection fields also refreshes discovery. The
  request waits for discovery before the UI reloads the plugin list.
  Activation is account-specific, so another user must activate the same
  shared plugin separately.
- In **Settings → Plugins**, select the provider and choose **Refresh models**
  to check its catalog explicitly. The model table is read-only and shows the
  IDs configured or discovered for the current account. A transient discovery
  failure keeps the previous discovered catalog, or the plugin's fallback
  `model_map` when no previous result exists, so a completed check does not by
  itself prove the remote endpoint is healthy.
- Automatic discovery requires an OpenAI-compatible `data` array of model IDs.
  Successful catalogs are stored per user without changing the shared plugin
  JSON. A normal activation keeps the user's previous catalog when discovery
  is unavailable. Changing or resetting a connection field clears that
  obsolete catalog first, so a failed refresh uses the plugin's existing
  `model_map`; configure those fallback model IDs in the plugin JSON when
  necessary.
- Image model availability, endpoint overrides, and API keys are also resolved
  for the current user. If an image request appears to use another account's
  provider settings, verify the request is authenticated as the expected user.
- If an upgraded non-admin account once stored a routing value, use **Reset**
  for that plugin. The ignored legacy value is purged so it cannot become active
  after a later role change. Saving or resetting routing also clears that
  account's discovered models so a stale catalog cannot follow the old route.
- Requests originate from the backend. When Libre WebUI runs in a container,
  `localhost` refers to that container, not automatically the host machine.
- Provider requests do not follow redirects. Configure the final validated
  operation URL directly.

### Chat Uses the Wrong Provider or Shows a Provider as Unavailable

The same model ID can exist in Ollama and in more than one plugin. Current Chat
sessions and default-model preferences save the selected provider as well as
the raw model ID, so similarly named entries are independent choices.

- If the selector says a provider is unavailable, reactivate or reinstall that
  exact plugin and confirm its model map still contains the saved model ID.
- If the provider or model was intentionally removed, explicitly select a
  replacement. Libre WebUI will not redirect an exact saved selection to a
  same-named model from another provider.
- Older sessions and preferences may have no provider metadata. Those records
  continue to use legacy name-only routing because Libre WebUI cannot infer
  which provider was originally intended. They appear as "provider not
  recorded" in model selectors. Reselect the desired Ollama or plugin entry to
  pin future requests to it.
- Persona entries remain labeled `persona:<id>`. Newly selected personas record
  Ollama as their backing provider; historical persona sessions without
  provider metadata remain compatible with legacy routing.

## Work Problems

### Work Is Missing or Reports Runtime Unavailable

Work requires a currently authenticated account with Work access — an
administrator, or any active user once an administrator has opened Work to
all users from the User Management page. Its container runtime
must be available to the Libre WebUI backend:

```bash
docker info
docker version
```

For the default Docker backend, confirm Docker is running and that the
operating-system user running Libre WebUI can invoke the configured
`WORK_DOCKER_COMMAND`. Installing Libre WebUI with `npx` does not install
Docker. If the runtime is missing, Libre WebUI keeps the rest of the
application available and does not fall back to executing model commands on
the host.

The repository Compose files enable Work by mounting the host Docker socket.
On Kubernetes, enable the native Pod/PVC runtime with Helm value
`work.enabled=true`; do not mount a node's runtime socket. When a Compose
deployment still reports **Runtime unavailable**, the Work page names which of
these applies:

| Message                                        | Cause and fix                                                                                                         |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `The "docker" CLI is not installed…`           | A custom image without `docker-cli`. Use the official image, or point `WORK_DOCKER_COMMAND` at a CLI.                 |
| `No Docker daemon is reachable…`               | The socket mount was removed, or the host daemon is stopped. Restore the mount in your Compose file and start Docker. |
| `The Docker socket is mounted but…cannot open` | The socket's group differs from the container's. Set `DOCKER_GID` in `.env` (see below) and recreate the container.   |

Read the socket group through a container, because a macOS host reports a
different value than the container sees:

```bash
echo "DOCKER_GID=$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  alpine stat -c '%g' /var/run/docker.sock)" >> .env
docker compose up -d --force-recreate
```

That socket grants root-equivalent control of the Docker host; review
[Work: Isolated Workspaces](./WORKSPACES) for what that means for your
deployment.

### The Model Lacks Tool Support

Work requires a tool-capable chat model. For Ollama, choose an installed model
whose reported capabilities include `tools`. For a plugin-backed model:

- Confirm the chat or completion plugin is active.
- Confirm the selected model is in that plugin's configured model list.
- Confirm an API key is available for the current administrator.
- Confirm the provider supports tool calls for that exact model.

Libre WebUI does not silently route a failed Work run to a different provider.

### A Work Request Returns HTTP 429

The instance has reached a task or active-runtime admission limit. By default,
Libre WebUI allows two active container-backed tasks across the instance and
one per user. A running preview also occupies runtime capacity. Wait for the
other operation to finish, stop an unused preview, or have the operator review
the `WORK_MAX_ACTIVE_RUNTIMES_*` and `WORK_MAX_TASKS_*` settings.

### Package Installation or Network Access Fails

New Work tasks use Docker bridge networking so generated projects can download
packages and start previews. Check Docker DNS, proxy configuration, registry
availability, and the command output in **Activity**. Libre WebUI does not mount
host SSH keys, cloud credentials, browser profiles, or the Docker socket into
the task container.

### A Work Preview Does Not Start

- Make sure the server binds to `0.0.0.0` on `WORK_PREVIEW_PORT` (`4173` by
  default).
- Leave the optional command empty to auto-detect a `package.json` `dev` script
  or a plain `index.html`, including a single nested app.
- If Work reports multiple apps or no supported entry point, enter the
  project's explicit development command in the optional command field. The
  command starts in `/workspace`, so use `cd <app-directory> && ...` for a
  nested app.
- Expand the returned error details to inspect startup output.
- Stop an existing preview before starting another command that needs the
  container.

Preview URLs use a dynamically assigned loopback port. The browser and Libre
WebUI backend therefore need to run on the same machine. A browser connected to
a remote backend cannot reach that backend's loopback preview, and an HTTPS page
may block a plain-HTTP preview as mixed content.

### A Workspace File Cannot Be Opened or Saved

The Work file API accepts UTF-8 text files up to 2 MB. If a file changed after
you opened it, reload it before saving so you do not overwrite the newer
version. Formatting is limited to supported file types under 100,000 characters
and 4,000 lines; syntax highlighting pauses for large files to keep editing
responsive.

Unsaved edits are kept as a draft in the current browser. They are not a
substitute for saving to the persistent workspace.

### A Task or Preview Was Stopped

Stopping a run, stopping a preview, or restarting Libre WebUI stops disposable
container processes but preserves the task's named workspace volume. Reopen the
task and restart its preview. Deleting the task is different: after
confirmation, it permanently removes the task and its workspace.

## Login and Signup Problems

**First user is not admin**

Only the first account created in a fresh database becomes admin. Existing databases keep their current users and roles.

**JWT errors**

Set a stable secret in production:

```env
JWT_SECRET=replace-with-a-long-random-secret
```

Changing `JWT_SECRET` invalidates existing sessions.

**Turnstile blocks signup**

Turnstile is enabled only when both keys are present:

```env
TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...
```

If signup suddenly fails, confirm the site key matches the domain and the secret key is valid.

**OAuth redirects fail**

Set callback URLs in both the provider dashboard and backend `.env`:

```env
BASE_URL=https://your-domain.example
GITHUB_CALLBACK_URL=https://your-domain.example/api/auth/oauth/github/callback
HUGGINGFACE_CALLBACK_URL=https://your-domain.example/api/auth/oauth/huggingface/callback
```

## Document Chat Problems

Libre WebUI currently accepts PDF and plain-text files up to 10 MB.

If search works but semantic retrieval does not:

1. Install an embedding model such as `nomic-embed-text`.
2. Enable embeddings in Settings.
3. Regenerate embeddings from the document settings or API.

```bash
ollama pull nomic-embed-text
```

Keyword search continues to work when embeddings are disabled.

## Artifact Preview Problems

For games or interactive HTML, ask the model for one complete self-contained HTML file with inline CSS and JavaScript.

If the artifact needs keyboard input:

- Click inside the preview first.
- Use the Open button to run it in its own browser tab.
- Avoid relying on local files that were not included in the response.

Libre WebUI can bundle common `index.html` + CSS + JavaScript code blocks, but self-contained HTML is still the most reliable output.

## Docker Problems

**Container cannot reach Ollama**

Use the external Ollama compose file when Ollama is not in the same compose stack:

```bash
docker compose -f docker-compose.external-ollama.yml up -d
```

**Data does not persist**

Mount a persistent data volume and set `DATA_DIR` if needed. The encryption key is stored in persistent storage when `DATA_DIR` or Docker mode is used.

## Resetting Local Data

Stop the app first. Then back up and remove the data directory you are using. By default, development data lives under `backend/data`.

```bash
cp -R backend/data backend/data.backup
rm -rf backend/data
```

Restart the backend and create a fresh account.

## Still Stuck

Open an issue with:

- Libre WebUI version and commit
- Install method
- Operating system
- Node.js version
- Ollama version
- Docker version and `docker info` result for Work problems
- Backend logs around the failure
- Browser console errors
- The exact model or provider being used
- Work Activity output when a task or preview fails

---

# Copyright & License

URL: https://docs.librewebui.org/copyright

# Copyright Header Management

This project automatically adds copyright headers to all TypeScript/JavaScript source files using a custom script.

## Copyright Header

All source files (`.ts`, `.tsx`, `.js`, `.jsx`) in the `frontend/src` and `backend/src` directories will automatically have this copyright header added:

```javascript
/*
 * Libre WebUI
 * Copyright (C) 2025 Kroonen AI, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
```

## Usage

### Automatic Header Addition

The copyright header is automatically added when you run:

```bash
npm run format
```

This command:

1. Runs the header addition script
2. Formats all files with Prettier

### Manual Header Addition

You can also add headers manually without formatting:

```bash
npm run add-headers
```

### Smart Header Detection

The script will:

- ✅ Add headers to files that don't have them
- ✅ Skip files that already have copyright headers
- ✅ Ignore configuration files, build outputs, and dependencies
- ✅ Only process TypeScript/JavaScript source files

### Ignored Files/Directories

The following are automatically ignored:

- `node_modules/`
- `dist/`
- `build/`
- `coverage/`
- `public/`
- Configuration files (`*.config.js`, `*.config.ts`)
- Type definition files (`vite-env.d.ts`)
- ESLint configuration (`eslint.config.js`)

## Implementation

The header management is implemented in `/scripts/add-headers.js` and integrated into the formatting workflow in `package.json`.

---

# Plugins

URL: https://docs.librewebui.org/plugin-architecture

# Plugins

Libre WebUI uses plugins to connect external AI providers and model capabilities alongside local Ollama.

## Plugin Types

| Type             | Purpose                                          |
| ---------------- | ------------------------------------------------ |
| Chat/completion  | Text and chat models from provider APIs          |
| Embeddings       | Vector embeddings for document search and memory |
| Image generation | Image models and ComfyUI-style backends          |
| Text-to-speech   | Voice synthesis providers                        |
| Speech-to-text   | Transcription providers                          |
| Audio generation | Sound and audio-generation providers             |
| Video generation | Asynchronous video-generation providers          |

Plugins can expose static model maps and, where supported, refresh available models from provider APIs.

## Built-In Provider Families

Libre WebUI includes provider definitions for common services:

- OpenAI and OpenAI-compatible APIs
- Anthropic
- Google Gemini
- Groq
- Kimi Code by Moonshot AI
- Mistral
- OpenRouter
- Hugging Face
- GitHub Models
- MLX LM for local Apple Silicon inference
- ComfyUI
- ElevenLabs

Provider catalogs change frequently. The UI should be treated as the source of truth for live model discovery when a plugin supports it.

## Ownership and Authorization

Plugin definitions are shared instance configuration. Every `/api/plugins`
route requires authentication, and only administrators can upload, install,
update, or delete a definition. Activation is different: each authenticated
user can activate or deactivate a shared plugin only for their own account.
That state is stored in SQLite and survives backend restarts without affecting
another user's active providers.

During upgrade, the legacy global `.status.json` activation list is copied once
to the accounts that already exist, but only for definitions that exactly match
Libre WebUI's compiled trust anchors. Legacy custom or shadow definitions stay
quarantined and inactive. Accounts created after that migration start with no
plugins activated.

Bundled definitions are trusted only when their normalized contents match a
hash compiled into the backend. Writable definitions are approved in SQLite by
normalized source path and full definition hash. An administrator install,
update, or re-import records that approval; direct file changes invalidate it.
Approval and updates clear every account's activation before replacing the
file, so each user must reactivate the reviewed definition. Pre-upgrade custom
definitions must be re-imported by an administrator before they can appear in
catalogs, discover models, accept credentials, or execute any capability.

Plugin variables are split by purpose. Only administrators can store recognized
connection-routing variables:

`endpoint`, `base_url`, `api_path`, `models_endpoint`, `api_url`,
`image_endpoint`, `embedding_endpoint`, `stt_endpoint`, `tts_endpoint`,
`voice_clone_endpoint`, `api_mode`, `model`, and `model_id`. A capability's
declared `config.endpoint_variable`, `config.models_endpoint_variable`, or
`config.voice_clone_endpoint_variable` is also connection routing, even when
it uses a different name.

Non-administrators can continue to save generation controls such as temperature
and streaming preferences. Old routing rows belonging to a non-administrator
are ignored, are not returned as configured values, and are removed by that
account's full plugin-variable reset. This prevents a later role promotion from
silently reviving a dormant route.

## Credentials

Credentials can come from environment variables or from user settings.

Environment examples:

```env
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GROQ_API_KEY=gsk_...
GEMINI_API_KEY=...
MISTRAL_API_KEY=...
OPENROUTER_API_KEY=sk-or-...
KIMI_API_KEY=...
GITHUB_API_KEY=github_pat_...
ELEVENLABS_API_KEY=...
```

For shared deployments, user-level credentials are usually better because each user controls their own provider billing and limits. Environment keys are useful for single-user installs, demos, or managed deployments.

An environment key is a fallback only while the request uses the routing and
authentication projection from an unshadowed bundled plugin definition. An
imported definition, a writable definition that shadows a bundled ID, or an
administrator's stored connection-routing override requires a credential saved
by the same account. Libre WebUI compares the root endpoint, authentication
fields, capability endpoints and endpoint-variable selectors, and recognized
routing-variable definitions and defaults before allowing environment fallback.
The compiled manifest hash remains authoritative even when the legacy and
bundled plugin directories share a path, as in the standard container layout;
an overwritten package manifest cannot establish its own trust.

This rule applies to discovery, Chat, Work, availability checks, and capability
catalogs. It prevents a custom endpoint or a pre-upgrade custom manifest from
receiving an operator-managed secret.

User-stored credentials are bound to the effective definition source, complete
definition hash, authentication contract, capability endpoints and selectors,
and effective routing values at the moment the user saves them. A route or
definition change makes the old credential unavailable until the user reviews
the new destination and saves the credential again. Legacy credentials without
a binding are accepted only on an exact anchored bundled route; their first
successful use writes the binding before returning the decrypted key.

## OpenAI-Compatible Providers

Many providers expose an OpenAI-compatible API. A plugin can define:

- Full API endpoint URL
- API key environment variable
- Chat endpoint behavior
- Embedding support
- Model discovery behavior
- Optional model map fallback

If a provider does not support live model discovery, Libre WebUI uses the configured model map.
Imported plugin JSON configures providers that already speak one of Libre
WebUI's supported wire formats: OpenAI Chat Completions, OpenAI Responses,
Anthropic Messages, or Gemini. JSON alone does not translate an arbitrary
proprietary protocol; a provider with a different request, streaming, tool-call,
or response shape needs a small backend adapter.

### OpenAI Image Generation

The bundled OpenAI provider exposes the Image API at
`https://api.openai.com/v1/images/generations`. `gpt-image-2` is the current
model. The catalog also retains the deprecated `gpt-image-1.5`, `gpt-image-1`,
and `gpt-image-1-mini` IDs for existing compatible deployments; new
configurations should select `gpt-image-2`.

Image generation uses the same effective OpenAI credential as Chat: the current
user's saved key, or the trusted bundled provider's environment fallback. It
has a separate optional `image_endpoint` override so a custom Chat endpoint
cannot accidentally receive image requests. Leave `image_endpoint` blank to
inherit the bundled Image API endpoint.

Image selections are provider-qualified. When two image plugins expose the same
model ID, Libre WebUI sends the request only to the provider selected in the
image panel. GPT Image responses use base64 image data; Libre WebUI converts
that data to an in-app image and saves it to the current user's gallery.
Image API routes require authentication, and direct generation requests must
include both `pluginId` and `model`. They may set `n` to a JSON integer from 1
through 10; numeric strings and fractional values are rejected before reaching
the provider.

### Chat Completions and Responses API modes

OpenAI-compatible completion plugins can use either `chat_completions` or
`responses` request semantics. The bundled OpenAI plugin exposes this choice in
**Settings → Plugins**.

Connection settings are resolved in this order:

1. A full `endpoint` override, when configured.
2. `base_url` plus an optional `api_path`.
3. The plugin's legacy `endpoint`.

An endpoint value that exactly matches the plugin manifest's endpoint is
treated as the manifest default, not as an override. This keeps legacy stored
defaults from shadowing a new Base URL after an upgrade. A genuinely custom
full endpoint still has highest precedence.

The default path is `/chat/completions` in Chat Completions mode and
`/responses` in Responses mode. `base_url` should be the API root, such as
`https://api.example.com/v1`; use `api_path` when a compatible provider exposes
the operation at a different relative path. A full endpoint must include the
complete operation path and takes precedence over both fields. A known
`/chat/completions`, `/completions`, or `/responses` suffix is authoritative for
request semantics; custom endpoint paths retain the selected `api_mode`.

Imported plugin JSON can provide the same defaults:

```json
{
  "endpoint": "https://api.example.com/v1/chat/completions",
  "api_mode": "responses",
  "base_url": "https://api.example.com/v1",
  "api_path": "/responses"
}
```

Responses requests use `input`, `max_output_tokens`, flattened function tools,
`store: false`, and request encrypted reasoning content for stateless
continuation. Completed and streamed Responses output is normalized back to
Libre WebUI's chat and Work event formats. Replay state is retained only when
the complete ordered output Item array is at most 64 Items and 90 KB; Items are
kept exact and are never field-truncated. Replayable Items require unique,
non-empty IDs and types, and message, reasoning, and function-call structures
are validated before any tool call is emitted. Oversized Chat state falls back
to normalized visible history. Chat also discards raw function-call Items
because Chat does not persist corresponding tool outputs. Tool-bearing Work
responses without bounded, exact replay state are rejected before any tool side
effect.

SQLite-backed Chat storage encrypts retained provider state with the message;
Work stores tool-only state in hidden context rows that are not returned by
message APIs. A hashed scope binds replay to the same provider, model, Responses
mode, final configured endpoint, and an opaque one-way fingerprint of the
selected credential. When that scope changes, including after API-key rotation,
Libre WebUI falls back to normalized message history rather than sending
provider-specific Items across an authentication boundary. An active Work run
also fingerprints its routing and credential and revalidates them immediately
before every provider round; changing the mode, endpoint, or API key stops the
run before another request can receive prior tool state.
Tool-bearing state must fit both the replay limit and the complete 100 KB
persisted metadata wrapper before Work performs a side effect. If a persisted
Work batch was interrupted, every missing tool result is restored with its
exact call ID and an outcome-unknown warning so the provider can inspect the
workspace instead of blindly repeating a possible side effect. An incomplete
Responses result is not treated as a successful Chat or Work turn; its
`incomplete_details.reason` is retained and surfaced to the caller.

Model discovery derives `/models` from either operation path. For example,
`https://api.example.com/v1/responses` discovers from
`https://api.example.com/v1/models`. Providers without a compatible model-list
endpoint can still use a manual `model_map`. Discovery is scoped to the current
user's variables and credentials. Results are persisted per user rather than
written into the shared plugin manifest. Discovery runs after activation,
explicit refresh, API-key changes, connection-variable changes, and variable
resets; unrelated generation-variable saves do not trigger a network request.

Discovery also runs on its own. Reading the plugin list rediscovers any active
completion provider whose catalog is missing or older than
`PLUGIN_MODEL_DISCOVERY_TTL_MS`, so reloading the application reflects the
provider's current models rather than the catalog captured at activation. A
per-provider backoff keeps an unreachable provider from being probed on every
request, and a deadline stops a slow provider from delaying the response; a
refresh that outruns it is served on the following request.
The final derived discovery URL is checked before the user's credential is read
or an authorization header is built, including when the URL originates in an
imported plugin manifest. Discovery and provider capability requests do not
follow HTTP redirects. Configure the final Chat, Work, model-list, image,
embedding, transcription, speech, voice-clone, audio, or video endpoint
directly; this prevents credentials from being forwarded from a validated URL
to an unvalidated redirect destination.

Provider endpoints may use HTTP or HTTPS. HTTP sends API keys, prompts, tool
results, and generated content without transport encryption, so use it only for
a self-hosted gateway on a network you trust; prefer HTTPS whenever the gateway
supports TLS. Requests originate from the backend. In container deployments,
that means a service URL such as `http://ai-gateway:8080/v1`, while `localhost`
identifies the Libre WebUI container itself. Plugin capability routes, including
image generation, resolve endpoint variables and credentials for the requesting
authenticated account. Libre WebUI does not have an unauthenticated
single-user mode.

### Capability-specific endpoints

Chat endpoint overrides are isolated from image, embedding, transcription,
text-to-speech, audio, and video capabilities. Multi-capability plugins can
expose `image_endpoint`, `embedding_endpoint`, `stt_endpoint`, `tts_endpoint`,
or another variable named by `config.endpoint_variable`. Voice-cloning routes
can likewise name `config.voice_clone_endpoint_variable`. Leaving those fields
blank uses the capability endpoint declared by the plugin; a generic Chat
`endpoint` is never used as a capability override.

The bundled GitHub Models plugin inherits its current
`models.github.ai/inference/chat/completions` endpoint when its optional
override is blank. The Hugging Face plugin uses task-specific
`hf-inference/models/{model}` routes and payloads for embeddings, images, and
text-to-speech rather than sending those requests to its Chat endpoint.

### Endpoint Overrides

The `endpoint` variable is the complete request URL, including the operation
path. For example, an OpenAI-compatible chat plugin normally uses a URL such as
`https://provider.example/v1/chat/completions`, not only
`https://provider.example`. Imported legacy plugin configurations may call this
variable `api_url`; Libre WebUI accepts that alias, but a non-empty `endpoint`
always takes precedence when both are present.

Absolute HTTP and HTTPS endpoint URLs are accepted; other protocols are
rejected. HTTP is intended for self-hosted gateways on trusted networks because
it sends credentials and request content without transport encryption. Prefer
HTTPS for any route that leaves a private deployment boundary. Leaving the
override empty uses the full endpoint from the plugin definition; an explicit
invalid override is rejected instead of silently routing to that default.

Provider requests do not follow redirects. Configure the final validated
operation URL directly; a redirect response is reported as a provider error
instead of forwarding credentials or request content to another hop.

Remember that requests originate from the Libre WebUI backend. In a container,
`localhost` identifies the container itself, not automatically the container
host or another service. Use the gateway's container service name, or a
host-reachable name such as `host.docker.internal` where the container runtime
provides it.

### Model Discovery

Settings → Plugins includes a **Provider connections** workspace for this
flow. Search for a provider in the left pane, select it, and use the right pane
to review its active state and effective model catalog. Provider configuration
remains collapsed until **Configure** is selected. This keeps endpoint,
credential, and advanced generation controls out of the default view.

For chat and completion providers, **Refresh models** runs discovery for the
selected provider and then reloads both the plugin catalog and Chat's model
list. The catalog is read-only: its rows come from the current user's
discovered IDs plus the plugin definition's capability model maps. Capability
labels describe which plugin route lists a model; they are not health checks.
Add fallback or manually maintained model IDs through the plugin JSON
`model_map`, not by editing a discovered row.

When a plugin is activated, Libre WebUI attempts model discovery with that
account's effective endpoint and credential. An administrator's custom route
requires a credential stored by the same account; an environment fallback is
used only with the trusted manifest route. For compatible APIs, Libre WebUI
derives a model-list URL from the full endpoint:

- a URL ending in `/models` is used as-is;
- known operation suffixes such as `/chat/completions`, `/completions`,
  `/responses`, `/embeddings`, or `/messages` are replaced with `/models`;
- otherwise, `/models` is appended to the path.

Plugins that cannot use the derived URL may expose `models_endpoint` as an
explicit full model-list URL. It takes precedence over derivation, is subject
to the same outbound URL policy, and is requested without following redirects.
Saving or resetting `endpoint`, `api_url`, `models_endpoint`, `base_url`,
`api_path`, or `api_mode` clears and refreshes the current user's discovered
catalog before the UI reloads it.

All custom routes are resolved and validated before credential selection. The
credential policy must not fall back to a server environment key for a stored
custom route; configure a per-user key for that route instead. Environment
fallback is reserved for the endpoint supplied by the trusted plugin
definition.

Discovery expects an OpenAI-compatible response containing model IDs in a
`data` array. Activation waits for that attempt before returning, so the first
plugin-list refresh can include the discovered catalog. Successful results are
stored per user and overlaid on that user's plugin view; Libre WebUI does not
rewrite the shared plugin JSON or expose one user's discovered model IDs to
another account. If the provider has no compatible model-list endpoint, cannot
be reached, or returns another response shape, an ordinary activation keeps
that user's previous discovery result. An intentional connection-field change
clears the obsolete catalog first and therefore uses the plugin's `model_map`
fallback when the new route cannot be discovered.

Saving or resetting connection routing clears that account's previous
discovered catalog before the next discovery attempt, so models learned from
one destination cannot remain selectable after a route change.

Plugin status, Work availability, model catalogs, and capability routes use the
same user context and credential boundary. For example, image model
availability, endpoint variables, and credentials are resolved for the user
making the request.

## Exact Provider Selection in Chat

Model IDs are not globally unique. An Ollama model and multiple active plugins
can all expose a model named `example-model`. Chat therefore stores the raw
model ID together with optional provider identity:

- `providerType: "ollama"` identifies the local or configured Ollama route;
- `providerType: "plugin"` plus `providerId` identifies one exact plugin.

Provider-qualified, URL-encoded values are used only as collision-safe keys in
model selectors. Requests continue to send the provider's raw model ID.
Duplicate Ollama/plugin and plugin/plugin model names remain separate choices,
and reopening a chat restores the exact choice that was saved.

Explicit provider identity fails closed. If a selected plugin is deactivated,
removed, or no longer advertises that model, Libre WebUI keeps the saved
selection visible as unavailable and does not silently switch to another
provider with the same model name. Reactivate the provider or explicitly choose
another model before generating again.

Sessions and preferences created before provider identity was stored can have
`providerType` and `providerId` unset or `null`. These legacy records retain
their historical name-only routing for compatibility because the original
provider cannot be reconstructed reliably. The selector shows these records as
"provider not recorded" rather than guessing an Ollama or plugin label.
Selecting a concrete provider entry records an exact provider for subsequent
requests. New persona selections keep their `persona:<id>` UI identity and are
recorded as Ollama-backed.

## Provider Settings and Inheritance

Open **Settings → Plugins** and choose **Configure** for a provider. Provider
panels are closed by default. Administrators can manage shared definitions and
connection-routing values. Other authenticated users can activate providers,
save their own API keys, and change their own generation controls, but the UI
does not expose plugin upload, install, export, delete, or routing controls to
them.

For administrators, connection overrides appear first. Sampling and other
specialist controls remain under **Advanced parameters**, which is also closed
by default. Inherited connection and generation values render as blank inputs
with a provider-default hint. Libre WebUI does not copy manifest defaults into
an account's saved settings merely because the panel was opened.

Saving sends only fields changed in the current editor session. Clearing a
saved non-sensitive value removes that account's override and restores the
provider default; a blank masked sensitive field is left unchanged. **Reset to
Defaults** removes every variable override that the account is allowed to
manage. If a save or reset fails, the editor keeps the unsaved values visible
so the user can retry.

This distinction is important for custom endpoints: an administrator leaves
the endpoint blank to inherit the plugin's bundled URL, or enters a complete
compatible URL to override it for that administrator's provider connection.

## Plugins in Work

Work can use active `completion` and `chat` plugins in addition to Ollama and
Ollama Cloud. A plugin-backed Work run is accepted only when:

- the plugin is active;
- its model is present in the current user's discovered catalog or the
  plugin's configured model map; and
- credentials are available for the current administrator.

Work keeps the selected provider type and plugin ID with both the task and each
run. Routing is therefore based on the exact saved provider, not only the model
name. Activating a plugin whose model name matches an Ollama model cannot
silently redirect an existing task.

Work adapts tool calls through native OpenAI-compatible, Anthropic, and Gemini
request/response formats. The selected model must support tool calling even if
the provider offers ordinary chat completions. If the provider rejects tools or
returns an incompatible response, the run fails without falling back to another
provider.

A remote Work run can make several provider requests. The provider receives the
Work system prompt, conversation context, tool definitions, and requested tool
results. Tool results can contain source files, directory listings, or command
output. Libre WebUI shows a per-user, dismissible remote-provider disclosure in
Work; operators should still review provider pricing, retention, and training
policies before enabling a service for sensitive projects.

## Embeddings

Embedding-capable plugins can appear in the document embedding settings. Libre WebUI also detects likely Ollama embedding models such as `nomic-embed-text`, `bge`, `e5`, `gte`, and similar model names.

When no embedding model is discovered, the UI falls back to `nomic-embed-text` as the local default candidate.

## Plugin Development Notes

A plugin definition should describe the capability clearly and avoid pretending a provider supports features it does not expose. Keep model maps small enough to be useful as fallbacks, and prefer discovery for providers with fast, reliable model-list APIs.

When adding a provider:

1. Add the plugin definition.
2. Define the credential key or user credential fields.
3. Implement model discovery if the provider offers a model-list endpoint.
4. Add request mapping for chat, embeddings, image, TTS, or STT.
5. Test missing-key, bad-key, and provider-error states.

## Related Docs

- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Working with Models](./WORKING_WITH_MODELS)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Kimi Code](./KIMI_CODE)
- [MLX LM on Apple Silicon](./MLX_APPLE_SILICON)

---

# Document Chat (RAG)

URL: https://docs.librewebui.org/rag-feature

# Document Chat

Document Chat lets Libre WebUI search uploaded documents and pass relevant excerpts into chat context.

## Supported Files

Current upload support:

- PDF
- Plain text
- Maximum file size: 10 MB

Files are processed by the backend and stored with the rest of the application data.

## Search Modes

Libre WebUI supports two retrieval modes:

| Mode            | When used                      | Notes                                                        |
| --------------- | ------------------------------ | ------------------------------------------------------------ |
| Keyword search  | Always available               | No embedding model required                                  |
| Semantic search | Embeddings enabled in Settings | Uses the configured embedding model and similarity threshold |

If embeddings fail or are disabled, document search falls back to keyword matching.

## Enable Semantic Search

Install an embedding model:

```bash
ollama pull nomic-embed-text
```

Then open Settings and enable embeddings. You can use local Ollama embedding models or embedding-capable provider plugins.

Default embedding settings:

- Model: `nomic-embed-text`
- Chunk size: 1000 characters
- Chunk overlap: 200 characters
- Similarity threshold: 0.3

## Upload and Search

1. Upload a PDF or text file from the document controls.
2. Wait for processing to finish.
3. Ask a question in chat.
4. Libre WebUI retrieves relevant chunks for that session and includes them as context.

Example prompts:

```text
Summarize the uploaded document in five bullets.
What deadlines are mentioned in the PDF?
Find the section that talks about pricing.
Compare the uploaded policy with this proposed change.
```

## API Endpoints

| Endpoint                                    | Purpose                           |
| ------------------------------------------- | --------------------------------- |
| `POST /api/documents/upload`                | Upload a PDF or text file         |
| `GET /api/documents`                        | List uploaded documents           |
| `GET /api/documents/session/:sessionId`     | List documents for a chat session |
| `POST /api/documents/search`                | Search documents                  |
| `DELETE /api/documents/:documentId`         | Delete a document                 |
| `GET /api/documents/embeddings/status`      | View embedding status             |
| `POST /api/documents/embeddings/regenerate` | Regenerate embeddings             |

## Best Practices

- Keep uploads focused on the current task.
- Use text-based PDFs when possible; scanned PDFs may have little extractable text.
- Regenerate embeddings after changing the embedding model.
- Lower the similarity threshold if semantic search misses useful context.
- Raise the threshold if results feel noisy.

## Related Docs

- [Working with Models](./WORKING_WITH_MODELS)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Troubleshooting](./TROUBLESHOOTING)

---

# SQLite Storage

URL: https://docs.librewebui.org/sqlite-migration

# SQLite Storage

Libre WebUI stores application data in SQLite by default. The storage layer keeps chats, messages, users, preferences, documents, document chunks, personas, plugin credentials, memories, and related metadata in one local database.

## Database Location

Source launches use this location order:

1. `DATA_DIR` when set; relative values resolve from the backend directory.
2. `backend/data` when it is unset.

For backward compatibility, an unset source profile keeps using
`backend/backend/data` when that is the only existing durable store. If both
locations contain state, startup requires an explicit selection and never
copies or merges them.

The packaged npm/Homebrew launcher instead defaults to `~/.libre-webui` and
resolves an explicit relative `DATA_DIR` from the caller's working directory.
Docker and Kubernetes deployments supply absolute container paths.

The SQLite file is named `data.sqlite`.

Example:

```env
DATA_DIR=/var/lib/libre-webui
```

## What SQLite Stores

- Users and roles
- Sessions and messages
- Preferences and UI settings
- Documents and chunks
- Personas and persona settings
- Persona memories and mutation state
- Plugin credentials and their routing/auth bindings, variables, per-user
  activations, writable-definition approvals, and discovered model catalogs
- System settings
- Work task ownership, model/provider routing, runs, messages, tool activity,
  status, and Docker resource identifiers

Sensitive values are encrypted at the application layer when they pass through the encrypted storage helpers.

## Work Storage Is Split

Work conversation and task metadata live in SQLite, but Work files do not. Each
task receives a dedicated Docker named volume mounted at `/workspace`. The
container is replaceable execution state; the named volume is the task's durable
filesystem.

This means a database backup by itself is not a complete Work backup. Back up
the corresponding Docker volumes using your Docker host's volume-backup process.
Libre WebUI labels managed Work volumes with
`ai.libre-webui.managed=true` and the owning task ID.

Deleting a Work task permanently removes its SQLite records and managed named
volume. Cancelling a run, stopping a preview, or restarting the backend does not
delete its files.

## JSON Compatibility

Older Libre WebUI installs used JSON files for some data. Current builds use SQLite as the primary storage path and keep storage access behind service/model layers so the rest of the app does not need to know the persistence format.

If you are upgrading an old install, back up the whole data directory before starting the newer backend.

Legacy plugin activation in `.status.json` is migrated once into per-user
SQLite rows for accounts that exist at upgrade time, but only for exact
hash-anchored bundled definitions. Legacy custom and shadow definitions remain
quarantined until an administrator re-imports them, and approval does not
restore old activation rows. Later accounts start with no active plugins, and
each account's activation changes are independent.

## Backup

Stop the backend before copying the database:

```bash
cp -R backend/data backend/data.backup
```

For deployments using `DATA_DIR`:

```bash
cp -R "$DATA_DIR" "$DATA_DIR.backup"
```

If the instance uses Work, also back up every managed Work named volume while
the backend is stopped. Keep the database, encryption key, and Work-volume
backup from the same point in time.

## Restore

Stop the backend, replace the data directory with your backup, then restart. Keep the same `ENCRYPTION_KEY`; encrypted values cannot be decrypted with a different key.

For Work, restore the named volumes under the exact names recorded in the
restored database before starting the backend. Libre WebUI can recreate a task
container, but it cannot reconstruct missing workspace files from the
conversation history.

## Operational Notes

- SQLite runs with WAL enabled for better concurrent reads.
- The data directory must be writable by the backend process.
- Keep `DATA_DIR` on persistent storage in Docker and Kubernetes.
- Back up `ENCRYPTION_KEY` together with the database.
- Account for Work named volumes separately when measuring, migrating, or
  restoring storage.

## Related Docs

- [Database Encryption](./DATABASE_ENCRYPTION)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker](./DOCKER)
- [Kubernetes](./KUBERNETES)
- [Environment Variables](./ENVIRONMENT_VARIABLES)

---

# Authentication & Security

URL: https://docs.librewebui.org/authentication

# Authentication & Security

Libre WebUI uses local user accounts with JWT sessions. A fresh installation
always permits one local administrator bootstrap. Public registration for every
later local or OAuth account is closed by default.

## First-Time Setup

When the database has no users:

1. Libre WebUI shows the first-time setup flow.
2. The user creates the first local account.
3. The account is assigned the `admin` role.
4. Every later public registration stays closed unless explicitly enabled.

Existing databases keep their current users and roles.

## Local Accounts

Local signup requires:

- Username
- Password between 12 characters and 72 UTF-8 bytes, with uppercase,
  lowercase, and a number
- Optional email

Passwords are hashed with bcrypt before storage. Login and signup routes are rate-limited.

## Registration Approval

Public registration does not grant access by itself. Every account created
through the public signup form or through an OAuth provider starts in a
`pending` state and must be approved by an administrator before it can sign in.

The one exception is bootstrap: the first real account on an empty database is
created `active` with the `admin` role, atomically, so a fresh install still
produces a working administrator. Every later registration waits for review.

What a pending user sees:

- Signup succeeds but returns no session token. The API responds `202` with
  `approvalRequired: true`, and the UI explains that an administrator has to
  approve the account.
- A password sign-in with correct credentials is refused with `403` and the
  code `ACCOUNT_PENDING` ("Your account is waiting for administrator
  approval"). An OAuth sign-in redirects back to the login page with
  `?approval=pending`.
- Account status is re-read from the database on every authenticated request,
  so a session can never outlive an account's `active` status.

What an administrator sees:

- User management shows a **Pending approvals** card listing waiting accounts,
  each with an **Activate account** action and a reject action. Rejection is
  deletion; there is no separate suspended state.
- Administrators are notified in the app while signed in: a badge on the Users
  entry and a toast when new registrations arrive. The pending-approvals
  summary is polled about once a minute
  (`GET /api/users/pending-approvals`, admin-only).
- Approval (`PATCH /api/users/:id/approve`, admin-only) records which
  administrator approved the account and when. It does not change the role:
  approved accounts keep the `user` role until an administrator promotes them.
  Approval takes effect on the user's next sign-in attempt; nothing needs to
  be recreated.

Existing accounts are unaffected by an upgrade: only accounts created through
public registration after the feature shipped start as pending. Accounts
created by an administrator from user management are active immediately.

### Enable public registration deliberately

Registration defaults to disabled. Set the backend environment variable below
only while new local or OAuth accounts should be accepted:

```env
ENABLE_SIGNUP=true
```

Return it to `false` after any planned registration window.
Existing local and OAuth users can still sign in, and administrators can still
create accounts from user management while public registration is closed.

An empty database always permits one local administrator, even when
`ENABLE_SIGNUP=false`; OAuth cannot claim that bootstrap slot. For a private
remote deployment, place the hostname behind an identity allowlist such as
Cloudflare Access before starting the application, then create the initial
administrator through that protected route.

## Roles

| Role    | Purpose                                                                                       |
| ------- | --------------------------------------------------------------------------------------------- |
| `admin` | Instance administration, user management, system settings, and trusted Work runtime operation |
| `user`  | Normal chat, model, persona, document, and settings workflows                                 |

Model installation, deletion, copying, pushing, and unloading are restricted to
administrators because these operations change host resources.

### Work Access

Work is restricted to administrators by default because it lets a selected
model execute arbitrary commands inside a managed container. An administrator
can open Work to all active users from the User Management page; the setting
persists across restarts and takes effect immediately, including for open
terminal sessions. Host-folder workspaces remain admin-only in every mode
because they bind-mount server paths. Treat everyone granted Work access as
a trusted runtime operator, not only as a WebUI user.

Admin authorization is checked against the current database role rather than
only the role cached in an existing JWT. Demoting an administrator therefore
revokes Work access immediately. The backend then attempts to abort active runs
and stop the user's Work containers and previews while preserving task records
and named volumes. If Docker cleanup fails, access remains revoked, the role
change reports the cleanup failure, and the operator must restore Docker access
and retry cleanup.

Deleting a user is destructive for that user's Work data. Libre WebUI first
stops their managed containers and removes their Work volumes, then deletes the
account and database records. If Docker cannot prove that cleanup succeeded,
the account deletion fails so an administrator can correct the runtime problem
and retry.

### Groups and Resource Grants

Administrators can create groups and manage memberships from the User
Management page. Groups are principals for resource grants: the owner of a
chat, note, document, knowledge collection, folder, or persona can grant
`read`, `write`, or `admin` access to a user or a group through the access
API. Resources stay private by default — the global `admin` role does not
grant access to other users' content. Membership is evaluated at request
time, so removing a member revokes group-granted access immediately. The
"effective access" view on the User Management page answers "why can this
user access this?" by listing their role, groups, feature access, and every
grant that reaches them.

### Security Audit Log

Security-sensitive actions — logins and failures, logouts, session and token
revocations, user, group, grant, and token changes — are recorded in an
append-only audit log that is separate from usage analytics. Details are
redacted before they are stored: secret-like keys are dropped and payload
sizes are capped, so passwords, tokens, and prompt content never enter the
log. Group and grant mutations write their audit event inside the same
database transaction, so a change cannot exist without its trail.
Administrators can query the log from the User Management page; retention
defaults to 180 days (`AUDIT_RETENTION_DAYS`).

## Sessions

The backend signs JWTs with `JWT_SECRET`. Set a stable secret in production:

```env
JWT_SECRET=replace-with-a-long-random-secret
```

Changing `JWT_SECRET` invalidates existing sessions. Local and OAuth login
tokens use `JWT_EXPIRES_IN`, which defaults to `7d`; changing that value affects
new sessions. WebSocket connections exchange the durable token for a
short-lived, one-use ticket and close when the underlying session expires.

Every login also creates a server-side session record bound into the JWT.
Settings → Sessions lists each device with its sign-in method, first and last
activity, and expiry. Revoking a session there (or "Sign out other sessions")
invalidates its token immediately on every replica and closes its live
WebSocket connections; logout revokes the current session the same way.
Tokens issued before this feature carry no session id and remain valid until
expiry, except that "sign out other sessions" from a fresh login also stamps
a per-account cutoff that rejects them.

## API Tokens

Settings → API keys mints personal access tokens (prefix `lwk_`) for
programmatic use. The secret is shown once and stored only as a hash. Each
token carries an explicit scope list (`chat`, `models`, `documents`, `notes`,
`personas`, `media`, `work`, `admin`); the backend maps every route family to
a required scope, so a notes-only token cannot touch chats or administration,
and session management is never reachable with a token. Tokens support
optional expiry, track last use, can be revoked at any time, and are
rate-limited per token across replicas. Admin-scoped tokens can be minted
only by administrators and still require the account to hold the admin role
when used.

## Cloudflare Turnstile

Turnstile protects password login and signup when both keys are configured:

```env
TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...
TURNSTILE_EXPECTED_HOSTNAME=chat.example.com
```

The frontend assigns distinct `login` and `signup` actions. The backend verifies
the token with Cloudflare and rejects a response whose hostname or action does
not match the request. `BASE_URL` supplies the expected hostname when
`TURNSTILE_EXPECTED_HOSTNAME` is not set explicitly.

If either key is missing, Turnstile is disabled.

## GitHub OAuth

Configure:

```env
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
GITHUB_CALLBACK_URL=https://your-domain.example/api/auth/oauth/github/callback
```

The GitHub OAuth flow creates local users with `gh_`-prefixed usernames and assigns the `user` role by default.

## Hugging Face OAuth

Configure:

```env
HUGGINGFACE_CLIENT_ID=...
HUGGINGFACE_CLIENT_SECRET=...
HUGGINGFACE_CALLBACK_URL=https://your-domain.example/api/auth/oauth/huggingface/callback
```

The Hugging Face OAuth flow creates local users with `hf_`-prefixed usernames and assigns the `user` role by default.

Both OAuth providers use a cryptographically random `state` value bound to a
short-lived HttpOnly, SameSite cookie. The callback rejects missing or mismatched
state. After a successful callback, the JWT crosses back to the frontend in a
60-second HttpOnly cookie that is exchanged and cleared immediately; bearer
tokens are never placed in callback URLs, browser history, or referrer headers.

## Redirects and CORS

Set `BASE_URL` for callback defaults and `CORS_ORIGIN` for browser access:

```env
BASE_URL=https://your-domain.example
CORS_ORIGIN=https://your-domain.example
```

For local development, include the Vite dev origin:

```env
CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173
```

## Demo Mode

Demo mode is a frontend preview mode. It pre-fills disabled demo credentials and uses mock API responses. It is not a production authentication mode.

## Security Checklist

- Set a strong `JWT_SECRET`.
- Keep `DATA_DIR` on persistent, access-controlled storage.
- Back up `ENCRYPTION_KEY` with the database.
- Configure Turnstile for public signup.
- Use HTTPS for public deployments.
- Restrict provider API keys to the minimum scope needed.
- Keep OAuth callback URLs exact.
- Grant Work access (administrator accounts, or the open-to-all-users mode)
  only to people trusted to operate the backend's container runtime.

## Related Docs

- [Single Sign-On](./SINGLE_SIGN_ON)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Database Encryption](./DATABASE_ENCRYPTION)
- [Demo Mode](./DEMO_MODE)

---

# Artifacts

URL: https://docs.librewebui.org/artifacts-feature

# Artifacts

Artifacts turn model-generated files and code blocks into a previewable side panel. They are useful for games, diagrams, HTML demos, JSON payloads, scripts, and generated assets.

Artifacts are different from [Work](./WORKSPACES). An artifact is created from
a normal chat response and rendered in the browser; it does not give the model a
persistent filesystem or shell. A Work task keeps durable project files and
conversation history, and runs model tools inside a task-scoped Docker
container.

## Supported Inputs

Libre WebUI detects:

- Explicit `<artifact>` blocks
- Fenced code blocks with artifact-friendly languages
- Standalone full HTML documents
- Multi-file HTML bundles made from `index.html`, CSS, and JavaScript blocks
- JSX and TSX components
- Mermaid diagrams
- SVG blocks
- JSON blocks
- Code and text snippets

## Artifact Types

| Type      | Preview behavior                                                   |
| --------- | ------------------------------------------------------------------ |
| HTML      | Sandboxed iframe preview with scripts and interaction enabled      |
| React     | JSX or TSX compiled and mounted, with Tailwind and the library set |
| Mermaid   | Diagram drawn in the sandbox, themed with the application          |
| SVG       | Inline visual preview                                              |
| JSON      | Formatted code view                                                |
| Code/text | Syntax-highlighted code view                                       |

## Multi-File HTML Bundles

When a model returns related HTML, CSS, and JavaScript blocks, Libre WebUI tries to merge them into a runnable HTML artifact. It removes local stylesheet/script references and inlines matching generated CSS and JavaScript.

For the most reliable result, ask the model for one self-contained HTML file:

```text
Create a complete self-contained HTML file with inline CSS and JavaScript.
It should run in a browser without local files.
```

## Viewer Controls

Artifacts open in a resizable side panel with:

- Preview and Code tabs
- Copy
- Download
- Open in new window
- Expand/fullscreen controls where available

If an interactive artifact needs keyboard input, click inside the preview first or open it in a new window.

## Sandbox Behavior

HTML artifacts load through `GET /api/artifacts/sandbox`, a small backend
document that hosts the preview. The indirection matters: an `srcdoc` frame
inherits the embedder's Content Security Policy, and the application policy
forbids inline scripts, so artifacts rendered that way are blocked in
production. The sandbox host is fetched over the network instead, so it carries
its own policy, and the preview frame it creates inherits that one.

The artifact policy allows inline script and `eval`, because that is what an
artifact is made of, and names no host but this application's own. An artifact
can render and compute; it cannot fetch from a CDN, and it cannot call home.

The frame itself allows scripts, forms, modals, popups, pointer lock, and
downloads, but never `allow-same-origin`, so an artifact runs on an opaque
origin with no access to the application's cookies, storage, or DOM. Its
feature policy allows clipboard access, fullscreen, and gamepad input.

## The Artifact Runtime

Generated artifacts assume libraries are available. Rather than let them reach
a CDN, the application vendors what they ask for:

| Available to artifacts                                        | How it is reached                   |
| ------------------------------------------------------------- | ----------------------------------- |
| React, ReactDOM, Framer Motion                                | `import ... from 'react'`           |
| JSX and TSX                                                   | compiled in the frame by Babel      |
| Tailwind utilities                                            | generated from the markup, no build |
| Recharts, Chart.js, Plotly, D3                                | `import ... from '<name>'`          |
| Three.js with controls, loaders, environments and more addons | `THREE.OrbitControls`, or by import |
| Lucide icons, Lodash, MathJS, Papa Parse, Tone.js             | `import ... from '<name>'`          |
| Mermaid                                                       | `mermaid` artifacts, or by import   |

An artifact that reaches for a library outside this set gets a notice naming
it, rather than a blank preview and a policy error in the console.

The frame never fetches any of it. The application page — which carries the
user's session — loads the bundles it needs and inlines them into the artifact
document, and the artifact's own `import` statements are compiled to lookups
against a small registry rather than left as network module resolution.

That indirection is not incidental. A sandboxed frame has an opaque origin, so
the browser treats its requests as cross-site and sends no session cookie.
Behind an authenticating proxy — Cloudflare Access, Authelia, oauth2-proxy —
such a request comes back as a redirect to a login page, which the sandbox
policy then refuses to load, and the artifact fails with a Content Security
Policy error. Inlining removes the request, so artifacts behave the same on a
laptop and behind a corporate gate.

React artifacts are compiled and mounted in the sandbox: export the component
as the module default and it renders. Every library resolves React from the
same registry, so there is one React instance and hooks behave normally.

HTML artifacts that load a library from a CDN still work — a `<script>` or
`<link>` pointing at Tailwind, Chart.js, D3, Three.js, Papa Parse, Lodash,
Mermaid, React or Babel is replaced by the vendored build inline, in the same
document position, so inline scripts still find `Chart`, `d3`, or `React` when
they run. A library outside that set is unavailable; inline it instead.

Because the frame has an opaque origin, real `localStorage`, `sessionStorage`
and `document.cookie` throw. The sandbox supplies in-memory stand-ins so an
artifact that uses them keeps running; the contents last as long as the preview
does and are not shared with the application.

Artifacts still execute generated code: inspect untrusted HTML before
downloading or reusing it outside the preview, and do not place secrets in an
artifact.

## Better Prompts

For games:

```text
Build a complete browser game as one HTML file.
Use canvas.
Inline all CSS and JavaScript.
Show controls on screen.
Avoid external assets unless they are optional.
```

For dashboards:

```text
Create one self-contained HTML dashboard.
Use semantic HTML, responsive CSS, and no build step.
Include sample data inline.
```

For React components:

```text
Return one React component in a single jsx block.
Export it as the default export.
Style it with Tailwind classes.
Import anything you need from react, recharts, or lucide-react.
```

For SVG:

```text
Return only one valid SVG code block with width, height, and viewBox.
```

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Pro Tips](./PRO_TIPS)
- [Working with Models](./WORKING_WITH_MODELS)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Release Automation

URL: https://docs.librewebui.org/release-automation

# Release Automation

Libre WebUI releases are created from the repository root with the release
script. The script reads real git history since the previous version tag,
updates package versions, writes the changelog, runs release checks, commits the
release, and creates the version tag. GitHub is the build and binary publication
source; release metadata and named artifact links are mirrored to the project's
Forgejo repository.

## One-Time Local Setup

Install dependencies and enable the repository hooks:

```bash
npm install
npm run setup-hooks
```

The hook setup configures:

- `.githooks/commit-msg` for Conventional Commit validation
- `.githooks/pre-commit` for formatting checks
- `.gitmessage` as the local commit message template

## Create a Release

Run the release script from a clean worktree on the branch you intend to tag:

```bash
# Patch release
npm run release

# Minor release
npm run release:minor

# Major release
npm run release:major
```

The script automatically:

1. Checks that the working tree is clean and the next local tag is available.
2. Collects commit, file, dependency, locale, and unreleased changelog evidence.
3. Generates the release notes from that evidence.
4. Updates `package.json`, workspace package files, `package-lock.json`, the
   Helm chart and app versions, and `CHANGELOG.md`.
5. Runs `npm run release:check`, including formatting, lint, builds, tests,
   security audit, and the npm publish dry-run.
6. Only after every check passes, commits the release and creates the annotated
   version tag.

## Changelog Generation

Preview the next changelog section without changing files:

```bash
npm run changelog
```

Update `CHANGELOG.md` manually from the generated section:

```bash
npm run changelog -- update
```

By default, changelog generation can ask a local Ollama-compatible model for a
polished draft, then validates the result against the collected git evidence.
If AI is unavailable or the output looks unsafe, the script falls back to a
deterministic generator.

Useful overrides:

```bash
CHANGELOG_AI=0 npm run release:minor
CHANGELOG_AI_MODEL=glm-5.2:cloud npm run changelog
OLLAMA_BASE_URL=http://127.0.0.1:11434 npm run release
```

## Push a Release

After the release commit and annotated tag are created, publish only the exact
branch commit and tag shown by the script. The production branch is pushed to
Forgejo first, then GitHub, with followed tags disabled explicitly:

```bash
git -c push.followTags=false push \
  https://git.kroonen.ai/libre-webui/libre-webui.git \
  HEAD:refs/heads/main
git ls-remote \
  https://git.kroonen.ai/libre-webui/libre-webui.git \
  refs/heads/main

git -c push.followTags=false push \
  https://github.com/libre-webui/libre-webui.git \
  HEAD:refs/heads/main
git ls-remote \
  https://github.com/libre-webui/libre-webui.git \
  refs/heads/main
```

Both returned branch SHAs must equal the intended local release commit. Wait for
the required GitHub workflows for that exact commit to pass before publishing
the tag.

Confirm the version tag does not already exist on either service, then push that
one tag to Forgejo first and GitHub second:

```bash
git ls-remote \
  https://git.kroonen.ai/libre-webui/libre-webui.git \
  'refs/tags/vX.Y.Z' 'refs/tags/vX.Y.Z^{}'
git ls-remote \
  https://github.com/libre-webui/libre-webui.git \
  'refs/tags/vX.Y.Z' 'refs/tags/vX.Y.Z^{}'

git -c push.followTags=false push \
  https://git.kroonen.ai/libre-webui/libre-webui.git \
  refs/tags/vX.Y.Z:refs/tags/vX.Y.Z
git -c push.followTags=false push \
  https://github.com/libre-webui/libre-webui.git \
  refs/tags/vX.Y.Z:refs/tags/vX.Y.Z

git ls-remote \
  https://git.kroonen.ai/libre-webui/libre-webui.git \
  'refs/tags/vX.Y.Z' 'refs/tags/vX.Y.Z^{}'
git ls-remote \
  https://github.com/libre-webui/libre-webui.git \
  'refs/tags/vX.Y.Z' 'refs/tags/vX.Y.Z^{}'
```

Replace `vX.Y.Z` with the release tag. For an annotated tag, verify both the tag
object SHA and its peeled commit SHA. Never use `git push --tags`, which can
publish unrelated local tags.

## CI Release Path

Pushing a `v*` tag runs the GitHub release workflow. The workflow:

- Runs `npm run release:check`
- Builds Electron artifacts for macOS, Windows, and Linux
- Creates the GitHub release from the matching `CHANGELOG.md` section
- Mirrors the release record and named artifact links to Forgejo
- Builds Docker images
- Publishes the Helm chart with the same version as the release tag
- Publishes the npm package with `NPM_TOKEN`

The same check can be run locally before tagging:

```bash
npm run release:check
```

## Forgejo Release Mirror

The mirror uses a Forgejo personal access token stored as the encrypted GitHub
Actions secret `FORGEJO_TOKEN`. Give the token only the
`write:repository` scope, ensure its owner can write to
`libre-webui/libre-webui`, and never commit or print the token.

The mirror is deliberately idempotent. It looks up releases by tag, creates
only missing release records, reconciles their GitHub release metadata, and
skips artifact links that already exist. A retry after a network or workflow
failure therefore completes the missing work without duplicating releases or
assets.

Forgejo release assets are named external links to the corresponding public
GitHub `browser_download_url`. GitHub remains the binary host, while Forgejo
shows the same downloadable filenames without duplicating tens of gigabytes of
desktop artifacts. Source archives remain generated independently from the
exact tag on each service.

### Preview or Backfill One Release

Inspect what would change without writing to Forgejo:

```bash
node scripts/mirror-forgejo-releases.mjs --tag vX.Y.Z --dry-run
```

After loading `FORGEJO_TOKEN` and `GITHUB_TOKEN` into the process environment
from the maintainer's secret manager, mirror that release:

```bash
node scripts/mirror-forgejo-releases.mjs --tag vX.Y.Z
```

The exact tag must already exist on GitHub and Forgejo and resolve to the same
tag object and peeled commit before a release is mirrored.

### Preview or Backfill All Releases

Audit every GitHub Release against Forgejo:

```bash
node scripts/mirror-forgejo-releases.mjs --all --dry-run
```

Backfill every missing or incomplete Forgejo Release:

```bash
node scripts/mirror-forgejo-releases.mjs --all
```

`GITHUB_TOKEN` is required for `--all`, including dry runs, because exact tag
parity and asset discovery require more requests than GitHub's anonymous API
limit permits. `FORGEJO_TOKEN` is additionally required whenever `--dry-run` is
not used.

The `--all` path paginates both APIs and considers GitHub Release objects, not
every Git tag. A tag that intentionally has no GitHub Release remains tag-only
on Forgejo. Run the dry-run again after a backfill; it should report no pending
changes.

## Immutable Tag Policy

Published version tags are immutable. After a tag exists on either remote:

- Do not delete it.
- Do not force-push it.
- Do not move it to a corrected commit.
- Do not reuse its semantic version for different contents.

If published release contents are wrong, correct the source and changelog and
publish the next patch version. If only a release page or external asset link is
missing, rerun the idempotent mirror without touching the tag.

The Forgejo `v0.8.6` tag had a one-time, explicitly approved realignment during
the introduction of dual release mirroring. It repaired two historical tag
objects that described identical source trees but followed different commit
lineages. That audited migration is not a precedent for moving published tags.

## Helm Version Policy

The Helm chart `version`, chart `appVersion`, root package version, and release
tag intentionally use the same semantic version. The release script advances
them together, and CI rejects a mismatch.

The chart is published only from an immutable `v*` release tag. Do not publish
modified chart contents under an existing chart version. A chart change must go
through the next application release so it receives a new version.

Chart version 0.14.1 carries a one-time digest override because that release
predates semantic Docker tags. The digest identifies the verified
multi-architecture 0.14.1 image. The release script clears this override when it
creates the next release, after which the default image resolves to the chart
`appVersion`.

The Docker workflow publishes that semantic-version tag to GHCR and Docker Hub
from the same `v*` release tag. Helm publication waits up to 20 minutes for the
matching public Docker Hub image and fails instead of publishing a chart with a
missing default image. The bundled Ollama image remains independently
configurable and defaults to its upstream `latest` tag.

## Conventional Commits

Commit messages should use Conventional Commit format:

```text
<type>[optional scope]: <description>
```

Common types:

- `feat`: user-facing feature
- `fix`: bug fix
- `docs`: documentation update
- `refactor`: internal code restructuring
- `perf`: performance improvement
- `test`: test coverage
- `chore`: maintenance, release, or build work

Breaking changes use `!`:

```bash
git commit -m "feat!: remove deprecated endpoint"
git commit -m "fix(auth)!: change token validation"
```

## Troubleshooting

### Working Directory Is Not Clean

Commit or stash the local changes before releasing:

```bash
git status --short
git add .
git commit -m "fix: resolve pending changes"
```

### No Releasable Changes

Check the commits since the previous tag:

```bash
git log $(git describe --tags --abbrev=0)..HEAD --oneline
```

### Changelog Needs Manual Editing

Edit `CHANGELOG.md`, then commit the correction before publishing the tag:

```bash
git add CHANGELOG.md
git commit -m "docs: refine changelog"
```

### Roll Back a Local Release Commit

If neither the release commit nor tag has been pushed:

```bash
git tag -d v0.12.0
git reset --soft HEAD~1
```

If either remote already has the tag, do not delete or replace it. Fix the
problem on `main`, create the next patch release, and publish that new immutable
tag through the complete gate.

### Forgejo Mirror Is Incomplete

First verify that both remote tag object and peeled commit SHAs match. Then
preview and retry the affected release:

```bash
node scripts/mirror-forgejo-releases.mjs --tag vX.Y.Z --dry-run
node scripts/mirror-forgejo-releases.mjs --tag vX.Y.Z
```

An authorization failure means `FORGEJO_TOKEN` is missing, expired, owned by a
user without repository access, or lacks `write:repository`. A missing or
different remote tag must be investigated separately; the release mirror never
creates or moves Git tags.

## Maintainer Files

- `.gitmessage` - commit message template
- `.githooks/commit-msg` - Conventional Commit validation
- `.githooks/pre-commit` - formatting preflight
- `scripts/release.js` - release orchestration
- `scripts/mirror-forgejo-releases.mjs` - idempotent Forgejo release mirror and
  backfill
- `scripts/generate-changelog.js` - changelog preview/update command
- `scripts/lib/releaseNotes.js` - evidence collection and changelog generation
- `.github/workflows/release.yml` - tag-driven CI release workflow
- `.github/workflows/helm-publish.yml` - Helm validation and tag publication

For more information about Conventional Commits, visit
https://www.conventionalcommits.org/.

---

# Docker with External Ollama

URL: https://docs.librewebui.org/docker-external-ollama

# Docker with External Ollama

Use this setup when Ollama already runs on your host, another server, or a Tailscale/LAN machine.

## Work Availability

This Compose setup changes where Ollama runs; Work remains enabled through the
mounted host Docker socket. It grants Libre WebUI root-equivalent control of the
Docker host, so use it only where Libre WebUI administrators are also trusted
host administrators. On Linux, set `DOCKER_GID` in `.env` to the group owning
the socket. Remove the mount if Work is not wanted. See
[Work: Isolated Workspaces](./WORKSPACES).

## Prerequisites

Confirm Ollama is reachable:

```bash
curl http://localhost:11434/api/version
```

Pull a model if the instance is new:

```bash
ollama pull gemma3:4b
```

## Start Libre WebUI

From the repository root:

```bash
docker compose -f docker-compose.external-ollama.yml up -d
```

Open [http://localhost:8080](http://localhost:8080).

The compose file points `OLLAMA_BASE_URL` to `http://host.docker.internal:11434` by default.

## Custom Ollama URL

For a different host:

```bash
OLLAMA_BASE_URL=http://192.168.1.100:11434 docker compose -f docker-compose.external-ollama.yml up -d
```

For Linux hosts that do not resolve `host.docker.internal`, add this to the service:

```yaml
extra_hosts:
  - 'host.docker.internal:host-gateway'
```

## Data Persistence

The compose file stores Libre WebUI data in Docker volumes:

- `libre_webui_data`
- `libre_webui_temp`

Set stable secrets for production:

```env
JWT_SECRET=replace-with-a-long-random-secret
ENCRYPTION_KEY=replace-with-64-hex-characters
```

Back up the data volume and encryption key together.

## Network Access

The repository Compose file currently sets `CORS_ORIGIN` directly. A value in
your shell or `.env` file does not replace that literal. Edit the
`libre-webui.environment` entry or add an explicit Compose override, for example
`compose.origin.yml`:

```yaml
services:
  libre-webui:
    environment:
      CORS_ORIGIN: http://localhost:8080,http://192.168.1.50:8080
      BASE_URL: http://192.168.1.50:8080
```

Apply both files:

```bash
docker compose \
  -f docker-compose.external-ollama.yml \
  -f compose.origin.yml \
  up -d
```

For a public domain, use the exact HTTPS origin for both values and terminate
traffic with HTTPS.

## Useful Commands

```bash
docker compose -f docker-compose.external-ollama.yml ps
docker compose -f docker-compose.external-ollama.yml logs -f libre-webui
docker compose -f docker-compose.external-ollama.yml restart
docker compose -f docker-compose.external-ollama.yml pull
docker compose -f docker-compose.external-ollama.yml up -d
```

## Troubleshooting

**Libre WebUI cannot reach Ollama**

```bash
docker compose -f docker-compose.external-ollama.yml exec libre-webui \
  wget -O- http://host.docker.internal:11434/api/version
```

If that fails, use an explicit LAN/Tailscale IP in `OLLAMA_BASE_URL`.

**Models do not appear**

Check the Ollama host:

```bash
ollama list
```

The Model Manager reads from the configured Ollama instance.

**CORS errors**

Make sure `CORS_ORIGIN` matches the exact browser origin, including scheme and port.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker](./DOCKER)
- [Working with Models](./WORKING_WITH_MODELS)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Personas

URL: https://docs.librewebui.org/persona-development-framework

# Personas

Personas let you save a reusable assistant profile with its own model, system prompt, generation settings, avatar/background, and optional memory behavior.

## What a Persona Stores

- Name and description
- Model ID
- Avatar and background
- System prompt
- Temperature, top-p, top-k, context window, max tokens, and penalties
- Optional embedding model
- Optional memory settings
- Optional mutation/adaptation settings

Persona data is scoped per user.

## Create a Persona

1. Open **Personas** from the sidebar.
2. Create a new persona.
3. Choose a model.
4. Write the system prompt.
5. Tune generation parameters.
6. Save and start a chat with that persona.

## Recommended Uses

| Persona            | Useful settings                                      |
| ------------------ | ---------------------------------------------------- |
| Code reviewer      | Low temperature, concise prompt, coding model        |
| Editor             | Medium temperature, writing style instructions       |
| Research assistant | Document Chat enabled, embedding model installed     |
| Creative partner   | Higher temperature, larger max token budget          |
| Support agent      | Strict tone and response format in the system prompt |

## Parameter Limits

Libre WebUI validates persona settings before saving:

| Parameter      | Range         |
| -------------- | ------------- |
| Temperature    | 0 to 2        |
| Top P          | 0 to 1        |
| Top K          | 1 to 100      |
| Context window | 128 to 131072 |
| Max tokens     | 1 to 8192     |

## Memory

Persona memory stores selected interactions as searchable memories. Memories include content, context, timestamps, importance scores, memory type, access counts, decay data, and optional embeddings.

Memory features include:

- Memory status and statistics
- Core memory retrieval
- Memory wipe
- Similar-memory consolidation
- Global decay

Semantic memory search uses the configured embedding model. `nomic-embed-text` is a good local starting point.

## Mutation State

When mutation settings are enabled, Libre WebUI stores a runtime state for the persona. The mutation engine can track mood, learned preferences, interaction patterns, and a mutation log based on conversation signals.

This is stateful assistant behavior, not a guarantee of autonomous reasoning. Keep prompts and settings explicit for production workflows.

## Import and Export

Personas can be exported as JSON and imported into another Libre WebUI instance. Exports include the persona configuration and advanced settings where present.

There is also a DNA-style export route that includes the persona, memories, and mutation log when those records exist.

## Related Docs

- [Working with Models](./WORKING_WITH_MODELS)
- [Document Chat](./RAG_FEATURE)
- [Database Encryption](./DATABASE_ENCRYPTION)

---

# Development Branch Guide

URL: https://docs.librewebui.org/dev-branch

# 🧪 Development Branch Guide

Want to try the latest features before they're officially released? The `dev` branch contains cutting-edge improvements and experimental features that will eventually make it to the main release.

:::warning Experimental Software
The `dev` branch is **experimental** and may contain bugs, incomplete features, or breaking changes. Use it only if you're comfortable with potential instability and want to help improve Libre WebUI.
:::

## 🎯 What is the Dev Branch?

The development branch (`dev`) is where new features are tested before being merged into the stable `main` branch. It includes:

- **Latest features** not yet in stable releases
- **Bug fixes** being tested
- **Experimental improvements** to the UI and functionality
- **Performance optimizations** under development

## 🚀 How to Use the Dev Branch

### Docker Setup (Recommended)

The development Compose files mount the host Docker socket, so Work functions
by default when Docker is available. Task containers run on the host daemon and
show up in `docker ps`. On Linux, set `DOCKER_GID` in `.env` first.

**With External Ollama:**

```bash
# Clone the repository
git clone https://github.com/libre-webui/libre-webui.git
cd libre-webui

# Switch to dev branch
git checkout dev

# Start the dev image with external Ollama
docker compose -f docker-compose.dev.external-ollama.yml up -d
```

**Simple Docker:**

```bash
# Use the dev branch image
docker run -d -p 3000:3001 -v libre-webui:/app/backend/data --name libre-webui-dev --restart always ghcr.io/libre-webui/libre-webui:dev
```

### From Source

```bash
# Clone and switch to dev branch
git clone https://github.com/libre-webui/libre-webui.git
cd libre-webui
git checkout dev

# Install dependencies
npm install

# Start development server
npm run dev
```

### Testing Work

1. Start Docker and confirm `docker info` succeeds as the same user running the
   backend.
2. Start Libre WebUI from source with `npm run dev`.
3. Sign in as an administrator.
4. Select **Work** and use a tool-capable Ollama, Ollama Cloud, or configured
   plugin-backed model.

Run the focused backend provider and container-policy tests with:

```bash
npm run test:work
```

The tests validate the generated Docker policy, path containment, lifecycle and
capacity behavior, and OpenAI-compatible, Anthropic, and Gemini tool adapters.
See [Work: Isolated Workspaces](./WORKSPACES) for the full runtime boundary.

## 🔄 Staying Updated

The dev branch is updated frequently. To get the latest changes:

```bash
# Update your local dev branch
git pull origin dev

# Refresh the dev Compose stack
docker compose -f docker-compose.dev.external-ollama.yml pull
docker compose -f docker-compose.dev.external-ollama.yml up -d

# Or restart simple Docker
docker pull ghcr.io/libre-webui/libre-webui:dev
docker stop libre-webui-dev && docker rm libre-webui-dev
docker run -d -p 3000:3001 -v libre-webui:/app/backend/data --name libre-webui-dev --restart always ghcr.io/libre-webui/libre-webui:dev
```

## 🐛 Found a Bug? Help Us Improve!

Your bug reports are incredibly valuable! Here's how to report issues effectively:

### Before Reporting

1. **Check existing issues**: Search [GitHub Issues](https://github.com/libre-webui/libre-webui/issues) to avoid duplicates
2. **Try the stable version**: Confirm the bug exists only in dev (not in main branch)
3. **Reproduce consistently**: Can you make the bug happen again?

### How to Report Bugs

[**🐛 Report a Bug on GitHub**](https://github.com/libre-webui/libre-webui/issues/new)

**Include this information:**

```markdown
**Environment:**

- Branch: dev
- Version: [git commit hash or date]
- OS: [Windows/macOS/Linux]
- Browser: [Chrome/Firefox/Safari version]
- Setup: [Docker/Source/etc.]
- Docker: [version and whether `docker info` succeeds, for Work issues]
- Work model/provider: [exact route, when applicable]

**Bug Description:**
Clear description of what went wrong

**Steps to Reproduce:**

1. Go to...
2. Click on...
3. See error...

**Expected Behavior:**
What should have happened

**Actual Behavior:**
What actually happened

**Screenshots/Logs:**
[If applicable, add screenshots or error logs]

**Work Activity:**
[Relevant tool call/result or preview output, with secrets removed]
```

### Get Your Git Commit Hash

```bash
# Find your current dev branch commit
git rev-parse HEAD

# Or get a short version
git rev-parse --short HEAD
```

## 🏆 Contributing & Recognition

Using the dev branch makes you part of our testing community! Contributors are recognized in several ways:

### Recognition for Contributors

- **Listed in [CONTRIBUTORS.md](https://github.com/libre-webui/libre-webui/blob/main/CONTRIBUTORS.md)**
- **Mentioned in release notes** for significant contributions
- **Co-author attribution** in commit messages
- **Special thanks** in project announcements

### Current Contributors

Our amazing community includes:

- **[rob](https://github.com/kroonen)** - Project Maintainer
- **[jm](https://github.com/jmoney7823956789378)** - Network Access Enhancement
- **And more contributors!** Check the [full list](https://github.com/libre-webui/libre-webui/blob/main/CONTRIBUTORS.md)

### Want to Contribute Code?

1. **Fork the repository**
2. **Create a feature branch from `dev`**: `git checkout -b feature/amazing-feature dev`
3. **Make your changes**
4. **Submit a Pull Request against the `dev` branch**

See our [Contributing Guidelines](https://github.com/libre-webui/libre-webui/blob/main/CONTRIBUTORS.md#contribution-guidelines) for detailed instructions and our [Community Charter](./CHARTER) for the project's ethical guidelines and governance model.

### Pull Request Checks

Every pull request, including a stacked pull request into an intermediate
feature or fix branch, runs the `Format & Lint` workflow. Its independent jobs
check formatting, frontend and backend linting, TypeScript types, package and
regression tests, and the Playwright browser suite. Failed browser runs upload
their Playwright results for debugging.

The `Electron Dev Build` workflow also packages macOS, Windows, and Linux
artifacts. macOS pull-request builds retain the project's credential-free ad-hoc
signature so the packaged application can be verified before upload. The
pull-request workflow does not receive Developer ID or notarization credentials.

The `Docker Build Test and Push` workflow builds both amd64 and arm64 images for
every pull request, including stacked pull requests into intermediate branches.
Pull-request builds do not log in to a container registry, push image digests, or
publish a multi-architecture manifest.

Run the same application-level checks locally before opening a pull request:

```bash
npm run format:check
npm run lint
npm run test:package
npm run test:e2e
```

## ⚠️ Important Notes

### Data Safety

- **Backup your data** before switching to dev branch
- Work task files live in separate `libre-work-*` Docker named volumes. Back
  those up separately from the SQLite data directory before testing destructive
  task or user lifecycle changes.
- **Use a separate Docker volume** for dev testing:
  ```bash
  # Use different volume name for dev
  docker run -d -p 3000:3001 -v libre-webui-dev:/app/backend/data --name libre-webui-dev ghcr.io/libre-webui/libre-webui:dev
  ```

### Potential Issues

- **Breaking changes** may require configuration updates
- **Features may be incomplete** or change without notice
- **Performance** may vary as optimizations are tested
- **UI elements** might look different or behave unexpectedly

### When to Use Stable

Switch back to the stable `main` branch if you:

- Need reliability for important work
- Experience too many bugs
- Want a tested, stable experience

```bash
# Switch back to stable
git checkout main
docker compose -f docker-compose.external-ollama.yml pull
docker compose -f docker-compose.external-ollama.yml up -d
```

## 🌟 Join the Community

- **GitHub Discussions**: [Share ideas and ask questions](https://github.com/libre-webui/libre-webui/discussions)
- **Issues**: [Report bugs and request features](https://github.com/libre-webui/libre-webui/issues)
- **Contributors**: [See who's helping build Libre WebUI](https://github.com/libre-webui/libre-webui/blob/main/CONTRIBUTORS.md)

---

**Ready to help shape the future of Libre WebUI?** 🚀

Your testing, feedback, and contributions on the dev branch directly improve the experience for all users. Thank you for being part of our development community!

---

# Community Charter

URL: https://docs.librewebui.org/charter

# Libre WebUI Community & Ethical Charter

_Adopted June 2025 • Maintained by Kroonen AI, Inc._

---

## 1 Mission

Libre WebUI exists to provide a **free, privacy‑respecting, community‑driven interface** for local large‑language‑model workflows.
We pursue simplicity and user sovereignty above growth, hype, or outside investment.

## 2 Core Principles

| Principle               | Commitment                                                                                                                           |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Freedom**             | Source code will remain licensed under the **Apache License 2.0** in perpetuity.                                                     |
| **Privacy**             | **Zero telemetry**. No analytics, no tracking, no phone‑home code—ever.                                                              |
| **Transparency**        | All decisions, road‑maps, and governance discussions occur in public issues/PRs or recorded community calls.                         |
| **Inclusive Community** | We enforce a strict anti‑harassment policy. All contributors and users are welcome regardless of background, identity, or geography. |
| **Local‑First**         | The default experience never requires cloud services or external APIs. Remote options are opt‑in and clearly marked.                 |
| **No Relicensing**      | This project shall **never** be relicensed to more restrictive terms (e.g., dual‑license, source‑available, proprietary).            |
| **Ethical Funding**     | We accept donations or grants only if they do not impose control over roadmap, license, or community. **No VC equity.**              |

## 3 Governance Model

1. **Stewardship**
   Kroonen AI, Inc. maintains the Libre WebUI project and its public infrastructure under this charter.
2. **Technical Steering Committee (TSC)**
   _Composition_: minimum 3, maximum 7 active contributors (rotating annually).
   _Responsibilities_: roadmap approval, release signing, Code‑of‑Conduct enforcement.
3. **Decision Process**
   - Consensus‑seeking → majority vote if consensus fails within 7 days.
   - All votes happen in public GitHub issues.
4. **Code of Conduct**
   Libre WebUI follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) with a dedicated response team.

## 4 Contribution Guidelines (Summary)

- Submit PRs against `dev`; require at least **one approving review** from the TSC.
- All new features must include documentation and unit tests.
- Security issues: disclose privately at **security@kroonen.ai**; we follow a 30‑day coordinated release window.

## 5 Ethical Use & Limitations

Libre WebUI is **tooling**, but we discourage—and will actively oppose—uses that facilitate:

- Human‑rights abuses
- Mass surveillance
- Autonomous lethal weapons

We reserve the right to refuse contributions or sponsorships tied to such activities.

## 6 Enterprise Services

Kroonen AI offers **commercial support and services** for organizations deploying Libre WebUI at scale. Enterprise offerings do not change the open source license—the core product remains Apache 2.0 for everyone.

### Available Services

| Service                    | Description                                                                                            |
| -------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Custom Deployment**      | On-premise installation, Kubernetes/Docker configuration, and infrastructure optimization              |
| **SLA-Backed Support**     | Guaranteed response times, dedicated support channels, and priority issue resolution                   |
| **Custom Development**     | Feature development, API integrations, white-labeling, and custom model integrations                   |
| **Training & Onboarding**  | Team workshops, documentation customization, and admin training                                        |
| **Security & Compliance**  | Security reviews, penetration testing support, and deployment documentation for regulated environments |
| **Air-Gapped Deployments** | Fully offline installations for high-security environments                                             |

### Regulated Deployments

Libre WebUI's local-first, zero-telemetry design can help organizations build deployments for regulated environments, but compliance depends on the full deployment, policies, controls, and audit process:

- Data can stay inside your infrastructure when using local models and self-hosted storage.
- Remote providers are optional and should be reviewed under your own data-processing requirements.
- Air-gapped and private-network deployments are supported by the self-hosted architecture.
- Enterprise services can help document controls and prepare security evidence, but Libre WebUI itself is not a certification.

### Contact

**Enterprise inquiries**: enterprise@kroonen.ai

**General support**: hello@kroonen.ai

**Website**: https://kroonen.ai/services

## 7 Amendments

Changes to this charter require:

1. Public proposal (GitHub issue)
2. Discussion period of 14 days
3. **2/3 super‑majority vote** of the TSC

---

_This document is authoritative once merged to the `main` branch and signed by the current TSC._

---

# Database Encryption

URL: https://docs.librewebui.org/database-encryption

# Database Encryption

Libre WebUI includes an application-level encryption service for sensitive values before they are written to storage.

## Encryption Method

The backend uses AES-256-GCM through Node.js crypto. The encryption key must be 32 bytes, represented as a 64-character hex string:

```env
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
```

Generate a key:

```bash
openssl rand -hex 32
```

## Key Storage

Libre WebUI loads the key in this order:

1. `ENCRYPTION_KEY` from the environment.
2. A persisted `.encryption_key` file under the selected `DATA_DIR`.
3. On a fresh store only, a newly generated key that is durably written to
   `DATA_DIR/.encryption_key` before the database starts.

If both the environment and persistent file provide a key, they must match or
startup fails. Existing encrypted state without its original key also fails
closed; Libre never generates a replacement for an existing store.

## Important Key Rules

- Back up `ENCRYPTION_KEY` with the database.
- Do not rotate the key unless you have a migration plan for encrypted values.
- Losing the key means encrypted values cannot be recovered.
- Changing the key without re-encrypting data will make existing encrypted values unreadable.

## What This Protects

Encryption is applied by code paths that use the encryption service or encrypted storage helpers. It is designed for sensitive application values such as credentials and private user data handled by those helpers.

It is not full-disk encryption, SQLite page encryption, or end-to-end encryption between users and the browser. Use disk encryption and HTTPS for those layers.

### Work Data

Application-layer encryption does not encrypt an entire Work task. Work source
files and project dependencies are ordinary files in task-scoped Docker named
volumes. Work conversations, tool results, command output, and task metadata are
stored in SQLite and are not automatically encrypted merely because some
credential-storage paths use the encryption service.

Protect the Docker data root and `DATA_DIR` with access controls and disk
encryption when required. Back up the database, `ENCRYPTION_KEY`, and managed
Work volumes together. Sending a Work task to a remote model can also disclose
conversation context and requested file or command output to that provider;
storage encryption does not change that network boundary.

## Docker and Kubernetes

Set a stable key explicitly for production:

```env
ENCRYPTION_KEY=replace-with-64-hex-characters
DATA_DIR=/data
```

Mount `DATA_DIR` on persistent storage. In Kubernetes, store the key in a Secret and mount data on a PersistentVolume.

## Troubleshooting

**Invalid key length**

The key must be exactly 64 hex characters. Generate a new one with:

```bash
openssl rand -hex 32
```

**Data cannot be decrypted after redeploy**

Confirm the same `ENCRYPTION_KEY` is used and the same `DATA_DIR` volume is mounted.

**Development generated a new key**

Keep `DATA_DIR/.encryption_key` with the database. You may instead set the same
value through `ENCRYPTION_KEY`; if both sources exist, they must match.

## Related Docs

- [SQLite Storage](./SQLITE_MIGRATION)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Docker](./DOCKER)
- [Kubernetes](./KUBERNETES)

---

# Single Sign-On

URL: https://docs.librewebui.org/single-sign-on

# Single Sign-On

Libre WebUI supports OAuth login with GitHub and Hugging Face, plus any OpenID Connect provider through the generic OIDC integration. OAuth users are still stored as local Libre WebUI users and receive the `user` role by default (OIDC can optionally map roles and groups from claims).

## GitHub OAuth

Create an OAuth app in GitHub Developer Settings.

Local callback URL:

```text
http://localhost:3001/api/auth/oauth/github/callback
```

Production callback URL:

```text
https://your-domain.example/api/auth/oauth/github/callback
```

Backend `.env`:

```env
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
GITHUB_CALLBACK_URL=https://your-domain.example/api/auth/oauth/github/callback
```

GitHub OAuth requests the `user:email` scope. New users are created with `gh_`-prefixed usernames.

## Hugging Face OAuth

Create an OAuth app in Hugging Face settings.

Local callback URL:

```text
http://localhost:3001/api/auth/oauth/huggingface/callback
```

Production callback URL:

```text
https://your-domain.example/api/auth/oauth/huggingface/callback
```

Backend `.env`:

```env
HUGGINGFACE_CLIENT_ID=...
HUGGINGFACE_CLIENT_SECRET=...
HUGGINGFACE_CALLBACK_URL=https://your-domain.example/api/auth/oauth/huggingface/callback
```

New users are created with `hf_`-prefixed usernames.

## Generic OIDC

Any provider with an OpenID Connect discovery document works: Keycloak,
Authentik, Authelia, Okta, Entra ID, Google Workspace, and others. The flow
uses PKCE (S256), CSRF state, and a nonce that is verified inside the
signature-checked ID token; signing keys come from the provider's JWKS.

Register a confidential client with this callback URL:

```text
https://your-domain.example/api/auth/oauth/oidc/callback
```

Backend `.env`:

```env
OIDC_ISSUER_URL=https://id.example.com/realms/main
OIDC_CLIENT_ID=libre-webui
OIDC_CLIENT_SECRET=...
OIDC_DISPLAY_NAME=Example SSO
```

Optional policies:

```env
# Require a verified email in one of these domains
OIDC_ALLOWED_EMAIL_DOMAINS=example.com,example.org

# Grant/remove the admin role based on a group claim on every login
OIDC_GROUP_CLAIM=groups
OIDC_ADMIN_GROUPS=libre-admins

# Reconcile Libre group memberships with the group claim on every login
OIDC_SYNC_GROUPS=true
```

Identities are linked on the stable `sub` claim, so a renamed provider
account keeps its Libre account. New users are created with `oidc_`-prefixed
usernames when registration allows it. An email already owned by an unlinked
local account is rejected instead of silently merged. With
`OIDC_SYNC_GROUPS=true`, membership of every Libre group whose name matches a
claim value is claim-driven for OIDC users — create matching groups from the
User Management page first.

## Shared Settings

Set the public backend URL:

```env
BASE_URL=https://your-domain.example
CORS_ORIGIN=https://your-domain.example
```

If callback URLs are not set explicitly, Libre WebUI builds defaults from `BASE_URL`.

## Limits

Libre WebUI does not currently expose SAML or SCIM provisioning. Domain allowlists and role/group mapping are available for the generic OIDC provider only; GitHub and Hugging Face users are always created with the `user` role and managed from the admin UI.

## Troubleshooting

**OAuth button does nothing**

Confirm the provider client ID and secret are set and the backend has restarted.

**Provider rejects callback**

The callback URL in the provider dashboard must exactly match the URL used by Libre WebUI.

**User gets normal permissions**

OAuth users are created as `user` by default. Promote users from the admin UI if needed.

## Related Docs

- [Authentication](./AUTHENTICATION)
- [Environment Variables](./ENVIRONMENT_VARIABLES)

---

# Desktop App

URL: https://docs.librewebui.org/electron-desktop-app

# 🖥️ Desktop App

The Libre WebUI desktop app wraps the web interface in a native window for
macOS, Windows, and Linux. It connects to a Libre WebUI server: one running on
the same machine, or any server you point it at.

The app is developed in
[its own repository](https://github.com/libre-webui/libre-webui-desktop);
installers for every release are attached to the main repository's
[GitHub releases](https://github.com/libre-webui/libre-webui/releases).

## 📦 Download and Install

Grab the package for your platform from the
[latest release](https://github.com/libre-webui/libre-webui/releases/latest):

- **macOS (Apple Silicon)**: `Libre-WebUI-Frontend-{version}-mac-arm64.dmg`
  or `.zip`, or `brew install --cask libre-webui/tap/libre-webui-frontend`
- **Windows**: `Libre-WebUI-Frontend-Setup-{version}.exe` (installer) or
  `Libre-WebUI-Frontend-{version}.exe` (portable)
- **Linux**: `Libre-WebUI-Frontend-{version}.AppImage` or
  `Libre-WebUI-Frontend-{version}-{arch}.deb`

### macOS blocks the app as damaged

Libre WebUI temporarily uses an ad-hoc signature for macOS builds. This keeps
the application bundle structurally valid, but it does not identify the
publisher to Apple and cannot be notarized. After copying the application to
Applications, approve it in **System Settings → Privacy & Security → Open
Anyway**.

If macOS does not offer that option, remove the quarantine attribute only after
verifying that the application came from the official Libre WebUI release:

```bash
xattr -dr com.apple.quarantine "/Applications/Libre WebUI Frontend.app"
open "/Applications/Libre WebUI Frontend.app"
```

Do not disable Gatekeeper globally. Seamless distribution still requires a
Developer ID Application certificate and Apple notarization.

## 🔌 Connecting to a Server

The app is a client; it needs a Libre WebUI server. On first launch a landing
screen offers two paths:

- **Local**: the app probes for a server on your machine (port 3001, the
  default for the Docker image and `npx libre-webui`) and connects when its
  health check answers.
- **Remote**: enter the URL of any reachable Libre WebUI server, such as a
  homelab or team deployment. The app verifies it before connecting.

Your choice is remembered. To change servers later, use
**Libre WebUI → Switch Server…** in the menu bar. If a remembered server stops
answering, the app returns to the landing screen instead of a blank window.

To run a local server, see [Quick Start](./QUICK_START): Docker Compose,
`npx libre-webui`, and Homebrew all work.

## 🧰 Work in the Desktop App

The desktop package does not bundle the backend, Docker, or a Work container
runtime. Work availability is determined by the server it connects to:

- a native backend that can run `docker info` can create task-scoped Work
  containers and named volumes;
- the standard repository Compose deployment provides Docker-backed Work by
  mounting the host Docker socket, while a custom container deployment without
  a reachable runtime reports Work as unavailable;
- a Kubernetes server provides Pod/PVC-backed Work when the Helm chart is
  installed with `work.enabled=true`; and
- the app continues to support Chat when Work is unavailable.

Work files live on the server's Docker host or Kubernetes storage, not inside
the desktop app. Previews travel through Libre WebUI's signed same-origin
proxy, so local and remote desktop clients can use them when the server's
reverse proxy preserves HTTP and WebSocket traffic.

## 🎨 Desktop Integration

- Native menu bar with app, edit, view, window, and help menus
- Custom macOS title bar (`hiddenInset`) with traffic lights in the sidebar
- Dark mode follows the system preference
- External links open in your default browser, never inside the app
- The renderer runs with context isolation and without Node integration

## 🚧 Limitations

- **macOS architecture**: current macOS packages support Apple Silicon
  (`arm64`) only
- **Requires a server**: the app does not bundle the backend
- **Work depends on the server runtime**: no Docker or Kubernetes runtime is
  included
- **No auto-updates**: updates require downloading a new package

---

See [Work: Isolated Workspaces](./WORKSPACES) for runtime, provider, storage, and
preview security details.

---

# Docker

URL: https://docs.librewebui.org/docker

# Docker

Docker is the easiest production-style deployment for a single server.

For an Internet-reachable single-server installation, start with
[Private Remote Deployment](./PRIVATE_REMOTE_DEPLOYMENT). Its Compose template
publishes no application ports, defaults to the `main` image, and adds an outer
Cloudflare Access boundary, host controls, backups, and container limits.

## Work Availability

Work is enabled in repository Compose files by default. The image ships the
Docker CLI and Compose mounts `/var/run/docker.sock`, so Work task containers
are **siblings** of the Libre WebUI container. They appear in `docker ps` on the
host.

A process with access to that socket has root-equivalent control of the Docker
host. Enabling Work by default is a deliberate choice: Work is a core feature,
and it cannot function without daemon access. The consequence is that **every
Libre WebUI administrator is effectively a host administrator**. Plan for it:

- Keep the stack on a host whose administrators you already trust.
- Do not expose the published port to an untrusted network.
- Remove the `/var/run/docker.sock` mount when Work is not required. The Work
  page then reports **Runtime unavailable**.

On Linux the socket belongs to the `docker` group instead of root, so the
non-root app user needs that group id. Set it once:

```bash
echo "DOCKER_GID=$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  alpine stat -c '%g' /var/run/docker.sock)" >> .env
```

Read it through a container as shown. A macOS host reports a different value
than the container sees, because Docker Desktop proxies the socket through a
VM. If the group is wrong, the Work page names the problem instead of failing
silently. See [Work: Isolated Workspaces](./WORKSPACES).

## Bundled Ollama

Runs Libre WebUI and Ollama in one Compose stack:

```bash
docker compose up -d
```

Open [http://localhost:8080](http://localhost:8080).

The WebUI port binds to host loopback by default. Set
`WEBUI_BIND_ADDRESS=0.0.0.0` only when a trusted LAN or a host reverse proxy must
reach it, and restrict the port with the host firewall.

Ollama remains private to the Compose network. To make it available to host
applications on loopback, add the explicit host override:

```bash
docker compose -f docker-compose.yml -f docker-compose.ollama-host.yml up -d
```

Set `OLLAMA_BIND_ADDRESS` only when another machine must reach Ollama, and
protect that port with a firewall and authentication-capable proxy.

## NVIDIA GPU

Use the GPU Compose file when Docker has NVIDIA runtime access:

```bash
docker compose -f docker-compose.gpu.yml up -d
```

Confirm GPU access from the Ollama container if models are still running on CPU.

## External Ollama

Use this when Ollama is already running on the host or another server:

```bash
docker compose -f docker-compose.external-ollama.yml up -d
```

Override the Ollama URL if needed:

```bash
OLLAMA_BASE_URL=http://192.168.1.10:11434 docker compose -f docker-compose.external-ollama.yml up -d
```

## Socket-Isolated Work

The standard Compose files mount the Docker socket into the Libre WebUI
container so Work can run task containers; that mount is root-equivalent
control of the Docker host. To keep Work without giving the web application
the socket, use the socket-proxy variant:

```bash
docker compose -f docker-compose.socket-proxy.yml up -d
```

A socket proxy on an internal network holds `/var/run/docker.sock` and
forwards only the API sections Work uses (containers, images, volumes,
networks, exec, info). Swarm, secrets, configs, build, and system endpoints
are denied at the proxy. Libre WebUI reaches it via
`DOCKER_HOST=tcp://docker-socket-proxy:2375` — no socket mount, no
`DOCKER_GID`, and the interactive terminal and system diagnostics work
unchanged. See the Workspaces documentation for what this boundary does and
does not cover.

## Data Persistence

Libre WebUI stores backend data in `/app/backend/data` inside the container. The Compose files mount that path as a named volume.

The image also uses that path by default when it is launched directly with
`docker run`; its database preflight scratch stays separately under
`/app/backend/temp`. Mount `/app/backend/data` whenever the container may be
recreated.

For production, set stable secrets:

```env
JWT_SECRET=replace-with-a-long-random-secret
ENCRYPTION_KEY=replace-with-64-hex-characters
```

Back up the data volume and encryption key together.

Before removing a directly launched container created from an older image,
confirm that `/app/backend/data/.encryption_key` exists or record the configured
`ENCRYPTION_KEY`. Older images could write an automatically generated key only
to the container layer; deleting that container also deletes the only key able
to decrypt its persisted database.

If you operate a custom Work-capable runtime, its task files live in separate
Docker named volumes and are not included in the normal
`/app/backend/data` backup.

## Public Access

Repository Compose files bind the WebUI to loopback and set `CORS_ORIGIN`
directly. A value in
your shell or `.env` file does not replace that literal. Edit the
`libre-webui.environment` entry or save an explicit override as
`compose.origin.yml`:

```yaml
services:
  libre-webui:
    environment:
      CORS_ORIGIN: https://your-domain.example
      BASE_URL: https://your-domain.example
```

Apply the override together with the selected repository Compose file:

```bash
docker compose -f docker-compose.yml -f compose.origin.yml up -d
```

Then put Libre WebUI behind HTTPS with a reverse proxy or platform load
balancer. Set `WEBUI_BIND_ADDRESS` to the exact interface that proxy needs; do
not publish the port on every interface unless the firewall requires it.

## Useful Commands

```bash
docker compose ps
docker compose logs -f libre-webui
docker compose logs -f ollama
docker compose pull
docker compose up -d
```

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker with External Ollama](./DOCKER_EXTERNAL_OLLAMA)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Database Encryption](./DATABASE_ENCRYPTION)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Kubernetes

URL: https://docs.librewebui.org/kubernetes

# Kubernetes

Libre WebUI ships a Helm chart under `helm/libre-webui`.

## Work on Kubernetes

Work runs natively on Kubernetes — no Docker daemon, CLI, or socket is
involved anywhere. Enable it at install time:

```bash
helm install libre-webui ./helm/libre-webui --set work.enabled=true
```

This switches the backend to `WORK_RUNTIME_BACKEND=kubernetes` and creates:

- a dedicated sandbox namespace (`work.namespace`, default
  `libre-webui-work`) holding one Pod per running sandbox and one
  PersistentVolumeClaim per task workspace (`work.workspaceSize`, default
  `5Gi` — a real per-task disk quota; a named Work policy can set a
  different size for the tasks created under it);
- a namespace-scoped Role and RoleBinding granting the backend's
  ServiceAccount exactly `pods` (get/list/create/delete), `pods/exec`
  (get/create), and `persistentvolumeclaims` (get/list/create/delete) in
  that namespace — no secrets, no cluster scope. This grant replaces the
  Docker socket entirely: the API server, not the application, enforces
  that a sandbox spec cannot mount host paths;
- NetworkPolicies that default-deny all sandbox traffic, allow ingress only
  from the backend on the preview port, and give network-enabled sandboxes
  egress to the internet minus `work.networkPolicy.blockedEgressCidrs`
  (private ranges, the CGNAT range some managed clusters use for pod and
  service CIDRs, and the cloud-metadata link-local range by default —
  verify your cluster's pod and service CIDRs are covered). Sandbox DNS is
  allowed only to `kube-system`; a cluster running node-local DNS needs its
  own DNS carve-out.

Sandboxes run non-root with a read-only root filesystem, all capabilities
dropped, seccomp `RuntimeDefault`, and no ServiceAccount token. Files,
commands, git, and interactive terminals ride the exec subresource through
the API server; preview is served from the sandbox Pod IP through the signed
same-origin proxy, which requires the backend to run in-cluster (the normal
chart topology). Host-folder workspaces are not supported on this backend.

Two operator notes. NetworkPolicy enforcement requires a CNI that
implements it (Calico, Cilium, recent kind releases, and most
managed-cluster defaults do) — verify with your cluster before treating
sandbox isolation as active; the CI end-to-end suite reports whether the
cluster it runs on enforces. And never mount a node's container-runtime
socket into the WebUI pod; the Kubernetes backend exists precisely so that
is unnecessary.

## Install

```bash
helm install libre-webui oci://ghcr.io/libre-webui/charts/libre-webui
```

The default chart deploys Libre WebUI with persistent storage and a bundled
Ollama service. The 0.14.1 transition is pinned to its verified
multi-architecture image digest; subsequent charts default to the matching
semantic `appVersion` image. Set `image.tag` or `image.digest` explicitly only
when you intentionally want a different image. A non-empty `image.tag` takes
precedence over the transition digest.

The default `solo` profile accepts `replicaCount: 0` for a deliberate suspension
or `replicaCount: 1` for normal operation. It rejects larger values and the
HorizontalPodAutoscaler because SQLite, local files, and process-local
coordination are not safe behind multiple pods. A zero-replica release
provisions its control-plane resources but serves no Libre WebUI traffic.

For multiple replicas, configure the complete `team` profile. It uses
PostgreSQL/PGVector, S3-compatible blob storage, Redis, and a separate durable
worker; the chart refuses a partial mixture of shared and local backends. Start
from a protected values file like this:

```yaml
replicaCount: 3

env:
  LIBRE_PLATFORM_MODE: team
  DATABASE_BACKEND: postgres
  DATABASE_SSL_MODE: verify-full
  POSTGRES_MIGRATION_MODE: apply
  POSTGRES_POOL_MAX: 10
  POSTGRES_CONNECT_TIMEOUT_MS: 5000
  POSTGRES_IDLE_TIMEOUT_MS: 30000
  POSTGRES_STATEMENT_TIMEOUT_MS: 30000
  POSTGRES_MIGRATION_LOCK_TIMEOUT_MS: 60000
  OLLAMA_TIMEOUT: 300000
  OLLAMA_LONG_OPERATION_TIMEOUT: 900000
  OLLAMA_MAX_CONTEXT: 32768
  BLOB_STORE_BACKEND: s3
  VECTOR_STORE_BACKEND: pgvector
  COORDINATION_BACKEND: redis
  JOB_WORKER_MODE: external
  STORAGE_ENCRYPTION_ACTIVE_KEY_ID: active
  S3_BUCKET: libre-blobs
  S3_REGION: us-east-1
  S3_BLOB_PREFIX: libre/blobs

worker:
  replicaCount: 1

secrets:
  databaseUrl: postgresql://libre:replace-me@postgres.example/libre
  redisUrl: rediss://redis.example:6379/0
  jwtSecret: '<one-stable-high-entropy-secret-for-every-replica>'
  encryptionKey: '<legacy-64-character-lowercase-hex-key>'
  storageEncryptionKeys: '{"legacy":"<legacy-64-character-lowercase-hex-key>","active":"<active-64-character-lowercase-hex-key>"}'
  s3AccessKeyId: replace-me
  s3SecretAccessKey: replace-me
```

`secrets.encryptionKey` must exactly match the `legacy` entry, and the key map
must also contain `STORAGE_ENCRYPTION_ACTIVE_KEY_ID`. `secrets.jwtSecret` must
be one stable, high-entropy value shared by every app and worker pod; the chart
rejects team mode without it so sessions never depend on pod-local generated
material. Keep verified TLS for managed PostgreSQL; do not add driver TLS
parameters to `databaseUrl`. Pool limits apply to every app and worker pod, so
reserve at least
`(replicaCount + worker.replicaCount) * POSTGRES_POOL_MAX` database connections
plus operational headroom. Install with the protected values file:

```bash
helm upgrade --install libre-webui \
  oci://ghcr.io/libre-webui/charts/libre-webui \
  --values /absolute/path/to/libre-team-values.yaml
```

Do not commit that file or pass production secrets through `--set`. Store it
with a protected encrypted-values workflow. Scale model providers and Work
sandbox Pods independently; when `work.enabled=true`, the external team worker
receives the same runtime image, StorageClass, and `work.env` limits as app pods.
The worker also receives the same resolved Ollama endpoint, request timeouts,
and maximum automatically adopted context as the app, because document
embeddings, durable chats, and Work runs execute provider calls there.
An active team application (a positive `replicaCount`, or enabled autoscaling)
requires at least one external worker, and the chart rejects a zero-worker
configuration before installation. Set both `replicaCount` and
`worker.replicaCount` to zero for a full suspension. Setting only the app count
to zero is a deliberate worker-only drain or recovery mode: no web traffic is
served, but the worker continues processing queued durable work.

### Team upgrades and schema compatibility

Libre supports an exact-schema-version policy, not mixed-version or zero-downtime
database upgrades. The application and external-worker Deployments each use
`Recreate`, which prevents old and new pods from overlapping within that one
Deployment. Kubernetes does not coordinate the two Deployments as a single
upgrade boundary. Before upgrading, stop new ingress, let or cancel active
durable and Work jobs, scale both old Deployments to zero, take a verified team
backup, and confirm every old app and worker pod has terminated. Only then
upgrade the release with `POSTGRES_MIGRATION_MODE=apply`; one new process holds
the PostgreSQL advisory leader lock while all other new processes wait and
validate the same migration ledger. Restore the previous verified backup into a
clean PostgreSQL/S3 target for rollback; never point an older binary at a schema
it does not exactly support. Expect an intentional service interruption during
this procedure.

## Access Locally

```bash
kubectl port-forward svc/libre-webui 8080:8080
```

Open [http://localhost:8080](http://localhost:8080).

## External Ollama

Use an existing Ollama endpoint:

```bash
helm install libre-webui oci://ghcr.io/libre-webui/charts/libre-webui \
  --set ollama.bundled.enabled=false \
  --set ollama.external.enabled=true \
  --set ollama.external.url=http://my-ollama:11434
```

## Secrets

Set a stable JWT secret and encryption key for production. The current chart
creates its own `<release>-libre-webui-secrets` object from `secrets.*` values;
it does not have an `existingSecret` setting, so pre-creating an unrelated
generic Secret does not wire those values into the pod.

```bash
helm upgrade --install libre-webui \
  oci://ghcr.io/libre-webui/charts/libre-webui \
  --set-string secrets.jwtSecret="$(openssl rand -hex 64)" \
  --set-string secrets.encryptionKey="$(openssl rand -hex 32)"
```

For production automation, supply stable values through an encrypted Helm
values workflow or an external-secrets integration you maintain; command-line
values can be exposed through process inspection and are retained in Helm
release metadata. The current chart exposes only the secret keys declared in
`values.yaml`. Add provider keys through a deliberate chart extension or
configure per-user credentials in the WebUI.

## Persistence

Keep the Libre WebUI data PVC and Ollama model PVC on persistent storage. Back up the Libre WebUI data volume and the encryption key together.

Work task workspaces live in their own PVCs in the sandbox namespace, not in
the Libre WebUI data PVC. Complete Work recovery needs both the database
(task ownership, resource names, runs) and those PVCs; back them up together
under the same policy.

## Ingress

For public access, configure ingress with HTTPS and set the exact browser
origin through the chart:

```bash
helm upgrade libre-webui \
  oci://ghcr.io/libre-webui/charts/libre-webui \
  --reuse-values \
  --set env.TRUST_PROXY=1 \
  --set-string env.CORS_ORIGIN=https://your-domain.example
```

`TRUST_PROXY` is an exact hop count, not a boolean. Its safe chart default is
`0`, which ignores forwarded client addresses. Use `1` only when one ingress
proxy connects directly to Libre; count every trusted load balancer or proxy
hop in a longer fixed chain and keep the Service unreachable around that
chain. A count that is too small groups clients under a proxy address and can
exhaust shared login limits; a count that is too large can trust a
client-supplied address. The chart accepts only `0` through `16`, never
unbounded `true`, and sends the value only to HTTP application pods.

The current chart does not expose `BASE_URL` or OAuth callback URL values.
Deployments using OAuth must extend the chart or patch the Deployment to set
those variables, and the callback URLs must match the public domain.

## Resource Planning

For local Ollama inside the cluster, schedule the Ollama pod on nodes with enough memory and GPU capacity for the models you plan to run. If your cluster already has a dedicated Ollama or inference service, external Ollama is usually simpler.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker](./DOCKER)
- [Hardware Requirements](./HARDWARE_REQUIREMENTS)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Authentication](./AUTHENTICATION)

---

# Hardware Requirements

URL: https://docs.librewebui.org/hardware-requirements

# Hardware Requirements

Libre WebUI's normal Chat interface is lightweight. Most resource demand comes
from local Ollama models; container-backed Work tasks add a separate CPU, memory,
process, image, and project-storage budget.

## Quick Reference

| Hardware                            | Practical local models | Notes                           |
| ----------------------------------- | ---------------------- | ------------------------------- |
| 8 GB RAM, CPU only                  | 1B-4B quantized        | Good for testing and light chat |
| 16 GB RAM, CPU only                 | 4B-8B quantized        | Usable, slower than GPU         |
| 8 GB VRAM                           | 4B-8B quantized        | Good daily local setup          |
| 12-16 GB VRAM                       | 8B-14B quantized       | Strong workstation range        |
| 24 GB VRAM                          | 14B-32B quantized      | High-end local use              |
| 48 GB+ VRAM or large unified memory | 32B-70B quantized      | Large model experimentation     |

Actual speed depends on model architecture, quantization, context length, GPU drivers, and what else is running.

## Good Starting Models

```bash
ollama pull gemma3:4b
ollama pull qwen3:8b
ollama pull deepseek-r1:8b
ollama pull nomic-embed-text
```

Use `nomic-embed-text` for document embeddings, not as a chat model.

## VRAM vs RAM

VRAM is the biggest factor for local model speed. If the model fits in VRAM, responses are much faster. If it spills into system RAM, it may still work but will be slower.

System RAM matters for CPU inference, GPU offload, long context windows, and running the rest of the app.

## Apple Silicon

Apple Silicon uses unified memory, so model memory comes from the same pool as the OS and applications. Larger unified-memory machines can run larger quantized models than their GPU VRAM number would suggest on a discrete-GPU system.

Keep enough free memory for the browser, backend, and operating system.

## NVIDIA

NVIDIA GPUs generally have the best local inference compatibility through CUDA. Use current drivers and confirm Docker GPU access if running Ollama in containers.

## AMD and Intel

AMD and Intel support depends on Ollama and driver support for your platform. CPU inference remains available when GPU acceleration is not.

## Reducing Memory Use

- Use smaller models.
- Use Q4 quantization instead of Q8.
- Lower context length.
- Unload models you are not using.
- Keep document embeddings separate from chat model choice.

## Work Runtime Capacity

Work adds container resources beyond the WebUI and model process. Each active
task container defaults to:

- 2 GB of memory;
- 2 CPUs; and
- 256 processes.

The backend allows two active container-backed tasks across the instance and one
per administrator by default. These are limits, not reservations, but operators
should budget for the WebUI backend, browser, Docker, Ollama, and task container
at the same time. On Apple Silicon, they all ultimately compete for the same
unified-memory pool.

Using Ollama Cloud or a configured remote model plugin can avoid loading a large
model into local RAM or VRAM. It does not remove the Docker requirement or the
resources needed by the Work container.

Every Work task also owns a Docker named volume for generated files and local
dependencies. Volumes do not currently have per-task disk quotas, so package
installs or generated projects can exhaust Docker storage. Monitor the Docker
data root, set host-level limits where available, and back up task volumes
separately from the Libre WebUI database.

Tune the `WORK_MEMORY_LIMIT`, `WORK_CPU_LIMIT`, `WORK_PIDS_LIMIT`, and
`WORK_MAX_ACTIVE_RUNTIMES_*` settings only after measuring the backend host. See
the [environment variable reference](./ENVIRONMENT_VARIABLES) for defaults.

## Related Docs

- [Working with Models](./WORKING_WITH_MODELS)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker](./DOCKER)
- [Kubernetes](./KUBERNETES)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Environment Variables

URL: https://docs.librewebui.org/environment-variables

# Environment Variables

This page lists the supported operator-facing environment variables read by
the current Libre WebUI backend, frontend, and maintenance scripts. Internal
test-only canaries are intentionally omitted.

## Backend Server

| Variable                     | Default                                                  | Purpose                                                                                       |
| ---------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `NODE_ENV`                   | `development`                                            | Runtime mode                                                                                  |
| `PORT`                       | `3001` in dev, `8080` in production                      | Backend HTTP port                                                                             |
| `TRUST_PROXY`                | unset (`0` in Helm)                                      | Exact trusted reverse-proxy hop count used to derive the client address                       |
| `CORS_ORIGIN`                | local dev origins                                        | Comma-separated allowed browser origins                                                       |
| `SERVE_FRONTEND`             | unset                                                    | Serve built frontend from backend when `true`                                                 |
| `DOCKER_ENV`                 | unset                                                    | Enables Docker-oriented behavior when `true`                                                  |
| `DATA_DIR`                   | `backend/data`; `~/.libre-webui` in the packaged CLI     | Persistent data directory                                                                     |
| `PLATFORM_PREFLIGHT_TMP_DIR` | `backend/temp/preflight`; user cache in the packaged CLI | Scratch space for a private DB/WAL startup inspection copy; size it for the database plus WAL |
| `PLUGINS_DIR`                | `$DATA_DIR/plugins`                                      | Writable directory for installed/customized plugins                                           |
| `BASE_URL`                   | `http://localhost:3001`                                  | Base URL used for OAuth callback defaults                                                     |
| `LOG_LEVEL`                  | `info` (`warn` in tests)                                 | Backend log level                                                                             |
| `WEBUI_HOST`                 | loopback; `0.0.0.0` in Docker                            | HTTP listen address                                                                           |
| `OPEN_BROWSER`               | `true` when serving the frontend                         | Set `false` to suppress automatic browser launch                                              |

Source launches anchor relative `DATA_DIR`, `PLUGINS_DIR`, and
`PLATFORM_PREFLIGHT_TMP_DIR` values at the backend directory, independent of
the shell's working directory. With `DATA_DIR` unset—or with the fresh source
example `DATA_DIR=./data`—root and backend workspace commands therefore use
`backend/data`. For compatibility, an existing source configuration containing
`DATA_DIR=./backend/data` continues to select `backend/backend/data`; change it
only as part of a deliberate, stopped backup and migration. An unset source
profile also keeps using `backend/backend/data` when it is the only existing
durable store. If both locations contain state and no path is selected, startup
fails closed instead of guessing, copying, or merging.

The `npx`, global npm, and interactive Homebrew launcher instead preserve data
under `~/.libre-webui`. An explicit relative `DATA_DIR` supplied to that
launcher resolves from the caller's working directory and is converted to an
absolute path before the backend starts. An explicitly configured relative
`PLUGINS_DIR` follows the same caller-relative rule; when it is unset, writable
plugins remain under `$DATA_DIR/plugins`. Inspection scratch defaults to a
user-writable cache outside the data directory: `~/Library/Caches/libre-webui`
on macOS, `%LOCALAPPDATA%\libre-webui` on Windows, or
`${XDG_CACHE_HOME:-~/.cache}/libre-webui` on other systems. The Homebrew service
pins the same home data directory and uses Homebrew `var/libre-webui/preflight`
for scratch. Set `PLATFORM_PREFLIGHT_TMP_DIR` explicitly when that cache cannot
hold the database plus its WAL. The supplied Docker and Helm deployments use
absolute `/app/backend/data` and `/app/backend/temp/preflight` paths backed by
separate mounts.

## Platform Foundation

The default `solo` profile uses SQLite, local encrypted blobs, encrypted
embedded vectors, local coordination, and an embedded durable worker. The
`team` profile uses PostgreSQL, private S3-compatible blobs, PGVector, Redis,
and an external worker. Team configuration is fail-closed: all shared
dependencies must be selected together.

| Variable                             | Default                                | Purpose                                                                                                                    |
| ------------------------------------ | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `LIBRE_PLATFORM_MODE`                | `solo`                                 | Select the coherent `solo` or `team` profile                                                                               |
| `DATABASE_BACKEND`                   | `sqlite`                               | Select `sqlite` or `postgres`                                                                                              |
| `DATABASE_URL`                       | unset                                  | PostgreSQL connection URL, required with `postgres`                                                                        |
| `DATABASE_SSL_MODE`                  | `verify-full`                          | PostgreSQL TLS policy: `disable`, `require`, or hostname-verifying `verify-full`                                           |
| `POSTGRES_MIGRATION_MODE`            | `apply`                                | Run compatible migrations under the leader lock, or use `validate` for a read-only schema check                            |
| `POSTGRES_POOL_MAX`                  | `10`                                   | Maximum PostgreSQL connections per application or worker process (1-100)                                                   |
| `POSTGRES_CONNECT_TIMEOUT_MS`        | `5000`                                 | PostgreSQL connection timeout (1-60000 ms)                                                                                 |
| `POSTGRES_IDLE_TIMEOUT_MS`           | `30000`                                | PostgreSQL idle connection timeout (1-600000 ms)                                                                           |
| `POSTGRES_STATEMENT_TIMEOUT_MS`      | `30000`                                | PostgreSQL statement timeout (1-600000 ms)                                                                                 |
| `POSTGRES_MIGRATION_LOCK_TIMEOUT_MS` | `60000`                                | Time to wait for the migration-leader lock (1-600000 ms)                                                                   |
| `BLOB_STORE_BACKEND`                 | `local`                                | Select encrypted `local` storage or private `s3`                                                                           |
| `VECTOR_STORE_BACKEND`               | `embedded` with SQLite                 | Select encrypted `embedded` vectors or `pgvector`                                                                          |
| `COORDINATION_BACKEND`               | `local` in solo; `redis` in team       | Select process-local or Redis coordination                                                                                 |
| `REDIS_URL`                          | unset                                  | `redis:` or `rediss:` URL, required with Redis coordination                                                                |
| `REDIS_KEY_PREFIX`                   | `libre`                                | 1-64 character namespace for Libre coordination keys                                                                       |
| `REDIS_CONNECT_TIMEOUT_MS`           | `5000`                                 | Initial Redis connection timeout, capped at 60 seconds                                                                     |
| `JOB_WORKER_MODE`                    | `embedded` in solo; `external` in team | Run handlers in the app or in the standalone shared worker                                                                 |
| `JOB_WORKER_CONCURRENCY`             | `4`                                    | Durable jobs one worker may run at the same time (1-32)                                                                    |
| `CHAT_STREAM_EVENT_RETENTION_HOURS`  | `24`                                   | Hours chat stream chunk events stay before the hourly sweep removes them                                                   |
| `PLATFORM_EVENT_RETENTION_DAYS`      | `30`                                   | Days any durable event stays before the hourly sweep removes it                                                            |
| `PLATFORM_JOB_RETENTION_DAYS`        | `30`                                   | Days finished non-lifecycle jobs stay before the hourly sweep removes them                                                 |
| `LIBRE_SKIP_STARTUP_INTEGRITY_SCAN`  | unset                                  | `1` skips the deep legacy-ciphertext scan on the next start (escape hatch; scan is otherwise cached per schema generation) |
| `STORAGE_ENCRYPTION_KEYS`            | unset                                  | Secret JSON key map; currently must include `legacy` matching `ENCRYPTION_KEY`                                             |
| `STORAGE_ENCRYPTION_ACTIVE_KEY_ID`   | unset                                  | Key ID used for new local blob and embedded-vector writes                                                                  |
| `BLOB_QUOTA_BYTES_PER_USER`          | `10737418240`                          | Durable maximum plaintext blob bytes per owner (positive safe integer)                                                     |
| `BLOB_QUOTA_RESERVATION_TTL_MS`      | `3600000`                              | Lifetime of an abandoned streaming quota reservation (at least 60000 ms)                                                   |
| `S3_BUCKET`                          | unset                                  | Private S3-compatible bucket, required with `s3`                                                                           |
| `S3_REGION`                          | unset                                  | S3 region, required with `s3`                                                                                              |
| `S3_ENDPOINT`                        | provider default                       | Optional absolute HTTP(S) endpoint for MinIO or another compatible service                                                 |
| `S3_ACCESS_KEY_ID`                   | SDK credential chain                   | Optional explicit S3 access key                                                                                            |
| `S3_SECRET_ACCESS_KEY`               | SDK credential chain                   | Required when an explicit access key is set                                                                                |
| `S3_SESSION_TOKEN`                   | unset                                  | Optional token accompanying explicit S3 credentials                                                                        |
| `S3_FORCE_PATH_STYLE`                | `false`                                | Set `true` for services that require path-style addressing                                                                 |
| `S3_BLOB_PREFIX`                     | `libre/blobs`                          | Opaque bucket-key prefix owned by Libre                                                                                    |

When the versioned storage key map is absent, storage adapters use the existing
`ENCRYPTION_KEY` as key ID `legacy`; when it is also absent, they read the
existing `${DATA_DIR}/.encryption_key` without generating or modifying it.
Explicit configuration and the persistent file must agree. If a versioned map
is introduced while a legacy key exists, retain that key under the exact ID
`legacy` until all objects and vectors have been rewritten or rewrapped and
verified. Conflicts, unsafe file permissions, symlinks, and missing configured
keys fail closed.

Redis is coordination, not canonical persistence. Selecting it alone does not
make SQLite, local files, or any other process-owned state safe across replicas.
In team mode, HTTP rate limits, chat/WebSocket connections, STT/TTS/audio
provider work, archive imports, and Work terminal sessions use Redis-backed
shared admission. Capacities apply across all replicas rather than once per
process. Admission and renewable-permit failures return `503` or abort the
in-flight operation; Libre never falls back to an independent local counter.
See [Platform Foundation](./45-PLATFORM_FOUNDATION.md).

The bundled team Compose profile and Helm chart forward every team platform
selector and tuning value above to both the application and external worker.
In the Helm chart, non-secret selectors live under `env`; set `secrets.redisUrl`,
`secrets.databaseUrl`, and `secrets.storageEncryptionKeys` for connection or
key material. PostgreSQL pool limits are per process: budget the database for
at least `(replicaCount + worker.replicaCount) * POSTGRES_POOL_MAX` connections,
plus operator and migration headroom. Keep `DATABASE_SSL_MODE=verify-full` for
managed or remote PostgreSQL. The bundled team Compose profile alone selects
`disable` because its database listener is isolated on the private project
network. Team Helm also requires one stable `secrets.jwtSecret` and mounts the
same Secret key into every application and worker pod; an omitted JWT secret
would otherwise generate process-local signing material. S3 receives opaque
keys and ciphertext; bucket URLs and provider URLs are not stored in
application metadata.

Integrated solo and team archives preserve the PostgreSQL pool and timeout
settings, Redis connection timeout, both blob-quota settings, platform
selectors, and S3 addressing settings in their signed and encrypted protected
configuration. A clean restore can therefore publish the operational values
needed to recreate the matching deployment without placing them in plaintext
archive metadata.

The bundled team Compose and Helm application/external-worker pairs receive the same resolved
`OLLAMA_BASE_URL`, `OLLAMA_TIMEOUT`, `OLLAMA_LONG_OPERATION_TIMEOUT`, and
`OLLAMA_MAX_CONTEXT`. Provider calls for document embeddings, durable chats,
and Work runs execute in the worker, so these values must not diverge between
processes. Both server entrypoints parse the three numeric values as complete
base-10 positive integers before creating local state or connecting to shared
state. Partial values such as `300000ms`, exponent/hex notation, out-of-range
values, and a long-operation timeout below the standard timeout fail startup.

Helm restricts `TRUST_PROXY` to an exact integer hop count from `0` through
`16` and forwards it only to HTTP application pods. Keep the default `0` for
direct traffic. Set the exact fixed count for an ingress/load-balancer chain;
never use the runtime's unbounded `true` form. A wrong count either groups
clients under a proxy address for shared rate limits or trusts an address a
client can supply.

PostgreSQL schema compatibility is exact-version. The Helm app and worker use
`Recreate`; drain and terminate every old pod before a team upgrade, then let
one new process migrate under the advisory leader lock. Do not run mixed binary
versions or claim a zero-downtime schema rollout. Rollback means restoring the
verified pre-upgrade team archive into clean PostgreSQL/S3 targets before
starting the matching older binary.

An active team application requires `worker.replicaCount >= 1`; Helm rejects a
live app with no durable worker instead of waiting for readiness to fail. Set
both application and worker counts to zero for a full suspension. App zero with
a positive worker count is a deliberate worker-only drain or recovery mode and
continues consuming queued jobs without serving web traffic.

## Private Backup Helper

These variables configure `deploy/private/libre-webui-backup` and are read by
the maintenance script, not the application process:

| Variable                            | Default                                           | Purpose                                              |
| ----------------------------------- | ------------------------------------------------- | ---------------------------------------------------- |
| `LIBRE_WEBUI_STACK_DIR`             | `/opt/libre-webui`                                | Directory containing the private Compose file        |
| `LIBRE_WEBUI_BACKUP_DIR`            | `/var/backups/libre-webui`                        | Protected directory for backup sets and lock file    |
| `LIBRE_WEBUI_BACKUP_RETENTION_DAYS` | `14`                                              | Age after which completed backup sets are removed    |
| `LIBRE_WEBUI_CONTAINER_NAME`        | `libre-webui`                                     | Deployed application container to inspect            |
| `LIBRE_WEBUI_BACKUP_KEY_DIR`        | `/etc/libre-webui/backup-keys`                    | Private archive-encryption and signing key directory |
| `LIBRE_WEBUI_RESTORE_IMAGE`         | required for restore                              | Reviewed immutable Libre image ID or digest          |
| `LIBRE_WEBUI_RESTORE_CONFIG_DIR`    | per-volume path below `/etc/libre-webui/restored` | New directory for recovered configuration            |

The systemd unit loads backup overrides from the optional root-owned file
`/etc/libre-webui/backup.env`. Set it to mode `0600`. Stack directory,
retention, container name, and backup-key directory can be set there directly.
The unit's filesystem sandbox permits writes only below the default backup
directory. A custom `LIBRE_WEBUI_BACKUP_DIR` additionally requires that exact,
pre-created directory in a `ReadWritePaths=` service drop-in; see
[Private Remote Deployment](./36-PRIVATE_REMOTE_DEPLOYMENT.md#backups-and-recovery).

## Authentication and Security

| Variable                      | Default                           | Purpose                                                  |
| ----------------------------- | --------------------------------- | -------------------------------------------------------- |
| `ENABLE_SIGNUP`               | `false`                           | Allow registration after the first local administrator   |
| `JWT_SECRET`                  | generated/fallback in development | JWT signing secret; set explicitly in production         |
| `JWT_EXPIRES_IN`              | `7d`                              | Session-token lifetime                                   |
| `ENCRYPTION_KEY`              | auto-generated                    | 64-character hex key for encrypted values                |
| `DEBUG_ENCRYPTION`            | unset                             | Logs encryption debug output when set                    |
| `TURNSTILE_SITE_KEY`          | unset                             | Cloudflare Turnstile site key for login and signup       |
| `TURNSTILE_SECRET_KEY`        | unset                             | Cloudflare Turnstile secret key for backend verification |
| `TURNSTILE_EXPECTED_HOSTNAME` | hostname from `BASE_URL`          | Required hostname in Cloudflare's verification response  |

Turnstile is enabled only when both Turnstile keys are present.

`ENABLE_SIGNUP=false` still permits the first local administrator on an empty
database, then blocks additional local and OAuth accounts. Protect a remotely
reachable bootstrap route with an outer identity boundary before first start.

Every issued JWT is bound to a server-side session (`sid` claim), so signing
out or revoking a session from Settings → Sessions invalidates the token
immediately on every replica and closes live WebSocket connections. Security
audit retention is configurable:

| Variable               | Default | Purpose                                             |
| ---------------------- | ------- | --------------------------------------------------- |
| `AUDIT_RETENTION_DAYS` | `180`   | Days to retain rows in the security audit event log |

### Generic OIDC single sign-on

Any OpenID Connect provider with a discovery document can be used for login.
The flow uses PKCE (S256), CSRF state, and a nonce verified inside the
signature-checked ID token. Identities are linked on the stable `sub` claim.

| Variable                     | Default                          | Purpose                                                                                |
| ---------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- |
| `OIDC_ISSUER_URL`            | unset                            | Issuer base URL; discovery is fetched from `<issuer>/.well-known/openid-configuration` |
| `OIDC_CLIENT_ID`             | unset                            | OAuth client id registered with the provider                                           |
| `OIDC_CLIENT_SECRET`         | unset                            | OAuth client secret                                                                    |
| `OIDC_DISPLAY_NAME`          | `Single Sign-On`                 | Label shown on the login button                                                        |
| `OIDC_SCOPES`                | `openid profile email`           | Requested scopes                                                                       |
| `OIDC_CALLBACK_URL`          | `BASE_URL` + OIDC callback route | Redirect URI registered with the provider                                              |
| `OIDC_ALLOWED_EMAIL_DOMAINS` | unset                            | Comma list; when set, a verified email in one of these domains is required             |
| `OIDC_GROUP_CLAIM`           | `groups`                         | ID-token claim holding group names                                                     |
| `OIDC_ADMIN_GROUPS`          | unset                            | Comma list; when set, the admin role follows claim membership on every login           |
| `OIDC_SYNC_GROUPS`           | `false`                          | `true` reconciles Libre group memberships with the group claim on every login          |

OIDC is enabled only when the issuer URL, client id, and client secret are all
present. An email already used by an unlinked local account is rejected rather
than silently merged, and account creation still honors `ENABLE_SIGNUP`.

Chat WebSocket admission can be tuned without weakening authentication:

| Variable                                  | Default | Purpose                                                 |
| ----------------------------------------- | ------- | ------------------------------------------------------- |
| `CHAT_WS_MAX_PAYLOAD_BYTES`               | 10 MiB  | Maximum accepted WebSocket message size                 |
| `CHAT_WS_MAX_MESSAGES_PER_MINUTE`         | `120`   | Per-connection WebSocket message ceiling                |
| `CHAT_WS_MAX_ACTIVE_GENERATIONS_PER_USER` | `4`     | Provider generations allowed per account                |
| `CHAT_WS_MAX_CONNECTIONS_PER_USER`        | `5`     | Concurrent authenticated sockets per account            |
| `WEBSOCKET_TICKET_TTL_MS`                 | `30000` | One-use Chat/Work ticket lifetime; capped at 60 seconds |

The browser exchanges its normal Authorization header for an opaque ticket and
puts only that short-lived value in the WebSocket upgrade URL. Tickets are
single use, protocol-bound, session-bounded, and stored only as hashes. This
keeps durable session tokens out of reverse-proxy request-target logs.
When `CORS_ORIGIN` or `BASE_URL` is configured, browser upgrades with an
`Origin` header must match one of those configured origins. Set at least one on
a remotely reachable deployment; with neither configured, the Origin filter
is permissive for local-development compatibility.
Originless upgrades remain supported intentionally for Electron and
non-browser clients, where browsers' Origin control is unavailable; they still
need a valid one-use ticket and receive the same current account, Work-access,
and task checks. Treat the ticket as the authentication boundary and restrict
non-browser access with the deployment's normal TLS, firewall, and reverse
proxy controls.

## OAuth

| Variable                    | Purpose                            |
| --------------------------- | ---------------------------------- |
| `GITHUB_CLIENT_ID`          | GitHub OAuth client ID             |
| `GITHUB_CLIENT_SECRET`      | GitHub OAuth client secret         |
| `GITHUB_CALLBACK_URL`       | GitHub callback URL override       |
| `HUGGINGFACE_CLIENT_ID`     | Hugging Face OAuth client ID       |
| `HUGGINGFACE_CLIENT_SECRET` | Hugging Face OAuth client secret   |
| `HUGGINGFACE_CALLBACK_URL`  | Hugging Face callback URL override |

If callback URLs are not set, Libre WebUI builds defaults from `BASE_URL`.

## Ollama

| Variable                        | Default                  | Purpose                                                                           |
| ------------------------------- | ------------------------ | --------------------------------------------------------------------------------- |
| `OLLAMA_BASE_URL`               | `http://localhost:11434` | Ollama API base URL                                                               |
| `OLLAMA_TIMEOUT`                | `300000`                 | Standard Ollama request timeout (1,000-3,600,000 ms)                              |
| `OLLAMA_LONG_OPERATION_TIMEOUT` | `900000`                 | Long operation timeout (1,000-3,600,000 ms and not shorter than `OLLAMA_TIMEOUT`) |
| `OLLAMA_MAX_CONTEXT`            | `32768`                  | Maximum model context adopted automatically (128-2,097,152 tokens)                |

## Web Search

| Variable      | Default | Purpose                                                                                             |
| ------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `SEARXNG_URL` | unset   | Default SearXNG endpoint for the web-search setting; an admin still enables it in Settings > Search |

## Libre Claw

| Variable                | Default                 | Purpose                         |
| ----------------------- | ----------------------- | ------------------------------- |
| `LIBRE_CLAW_BASE_URL`   | `http://127.0.0.1:8766` | Optional Libre Claw daemon URL  |
| `LIBRE_CLAW_TIMEOUT_MS` | `30000`                 | Libre Claw HTTP request timeout |

## Work Runtime

These variables configure Work execution on the machine or Kubernetes cluster
running the Libre WebUI backend. Docker is the default runtime; the Helm chart
selects Kubernetes when `work.enabled=true`.

| Variable                              | Default                                                                                       | Purpose                                                                                                     |
| ------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `WORK_RUNTIME_IMAGE`                  | `node:22.22-bookworm@sha256:2d178f2785b96dfbf62a416ca2e40f50e30150b4ff3320d706f0d96e90600eb3` | Pinned image used for Work sandboxes                                                                        |
| `WORK_DOCKER_COMMAND`                 | `docker`                                                                                      | Docker-backend CLI executable available to the process                                                      |
| `WORK_COMMAND_TIMEOUT_MS`             | `120000`                                                                                      | Default timeout; a tool can request up to `600000` ms                                                       |
| `WORK_MAX_OUTPUT_CHARS`               | `50000`                                                                                       | Captured stdout/stderr limit, applied to each stream                                                        |
| `WORK_MAX_AGENT_ROUNDS`               | `48`                                                                                          | Provider-agnostic model/tool round budget for one run                                                       |
| `WORK_MEMORY_LIMIT`                   | `2g`                                                                                          | Memory limit passed to each Work container                                                                  |
| `WORK_CPU_LIMIT`                      | `2`                                                                                           | CPU limit passed to each Work container                                                                     |
| `WORK_PIDS_LIMIT`                     | `256`                                                                                         | Process limit passed to each Work container                                                                 |
| `WORK_PREVIEW_PORT`                   | `4173`                                                                                        | Port a preview server must use inside the task container                                                    |
| `WORK_PREVIEW_BIND`                   | `127.0.0.1`                                                                                   | Host interface a task preview port is published on                                                          |
| `WORK_MAX_ACTIVE_RUNTIMES_GLOBAL`     | `3`                                                                                           | Concurrent runtime-backed tasks for the whole instance                                                      |
| `WORK_MAX_ACTIVE_RUNTIMES_PER_USER`   | `2`                                                                                           | Concurrent runtime-backed tasks for one user                                                                |
| `WORK_MAX_TASKS_GLOBAL`               | `500`                                                                                         | Maximum persisted Work tasks for the whole instance                                                         |
| `WORK_MAX_TASKS_PER_USER`             | `100`                                                                                         | Maximum persisted Work tasks for one administrator                                                          |
| `WORK_NETWORK_NAME`                   | `libre-webui-work`                                                                            | Managed sandbox bridge network for networked tasks                                                          |
| `WORK_RUN_LEASE_WAIT_MS`              | `60000`                                                                                       | How long a run waits for the task's shared runtime lease before reporting a replica conflict (team mode)    |
| `WORK_RUNTIME_DNS`                    | unset                                                                                         | Comma-separated resolver IPs forced onto networked tasks                                                    |
| `WORK_DOCKER_SOCKET`                  | `DOCKER_HOST` if `unix://` or `tcp://`, else `/var/run/docker.sock`                           | Docker Engine endpoint for terminals and diagnostics                                                        |
| `WORK_TERMINAL_MAX_SESSIONS_PER_TASK` | `2`                                                                                           | Simultaneous browser terminals attached to one task                                                         |
| `WORK_TERMINAL_IDLE_TIMEOUT_MS`       | `900000`                                                                                      | Idle timeout before a terminal session is closed                                                            |
| `WORK_RUNTIME_IDLE_TIMEOUT_MS`        | `0` (disabled)                                                                                | Stop a sandbox after this much inactivity (previews too)                                                    |
| `WORK_HOST_WORKSPACES_ENABLED`        | `false`                                                                                       | Allow a task to use a host folder instead of a volume                                                       |
| `WORK_HOST_WORKSPACE_ROOTS`           | the server user's home directory                                                              | `:`-separated roots a host workspace must live inside                                                       |
| `WORK_RUNTIME_BACKEND`                | `docker`                                                                                      | Sandbox backend: `docker` or `kubernetes`                                                                   |
| `WORK_K8S_NAMESPACE`                  | `libre-webui-work`                                                                            | Namespace holding Kubernetes sandbox Pods and PVCs                                                          |
| `WORK_K8S_STORAGE_CLASS`              | cluster default                                                                               | StorageClass for workspace PVCs                                                                             |
| `WORK_K8S_WORKSPACE_SIZE`             | `5Gi`                                                                                         | Per-task workspace PVC size (a real disk quota)                                                             |
| `WORK_K8S_POD_READY_TIMEOUT_MS`       | `900000`                                                                                      | Wait for a sandbox Pod to reach Running (covers pulls)                                                      |
| `WORK_K8S_POD_GONE_TIMEOUT_MS`        | `60000`                                                                                       | Wait for a deleted sandbox Pod to disappear                                                                 |
| `AGENT_CLI_MODELS_ENABLED`            | unset (admin toggle, off)                                                                     | Pin the Agents feature on/off; unset leaves it to the admin toggle in User Management (disabled by default) |
| `AGENT_CLI_TIMEOUT_MS`                | `600000`                                                                                      | Time an agent CLI may run before it is killed                                                               |
| `CODEX_OAUTH_MODELS_ENABLED`          | `true`                                                                                        | Offer the Codex (ChatGPT) provider to admins                                                                |
| `CODEX_HOME`                          | `~/.codex`                                                                                    | Where the Codex CLI sign-in (`auth.json`) is read from                                                      |

Agent CLI binaries and Codex OAuth credentials are node-local. They are
supported only in a solo process, where discovery and execution see the same
filesystem and environment. Team mode executes durable chat jobs in an
external worker, so it requires both `AGENT_CLI_MODELS_ENABLED=false` and
`CODEX_OAUTH_MODELS_ENABLED=false`; startup rejects any other value rather
than advertising a provider that may exist only on an application replica.
Use Ollama or a provider plugin whose credentials and routing are stored in
shared PostgreSQL or forwarded identically to every application and worker.

On the Docker backend, a host workspace bind-mounts a real directory at
`/workspace`, so the task can read and write those files directly instead of
working in its own Docker volume. Kubernetes rejects host-folder workspaces.
This is a deliberate reduction of the Docker sandbox: keep
`WORK_HOST_WORKSPACES_ENABLED` off unless you want it, and keep
`WORK_HOST_WORKSPACE_ROOTS` as narrow as possible. Requested paths are resolved
through symlinks before they are checked against the roots, and folders such as
`.ssh`, `.gnupg`, `.aws`, and `.config` are rejected outright.

Agent CLI models expose coding agents already installed on the server (`claude`,
`codex`) as selectable chat models, so a subscription agent can answer without an
API key. Only administrators see them, the CLI runs as the Libre WebUI server
user, and it inherits that user's agent credentials — treat it as equivalent to
granting shell access to those agents.

On Docker, networked Work tasks attach to the managed `WORK_NETWORK_NAME`
bridge, created with inter-container communication disabled so one sandbox
cannot reach another sandbox or the deployment's own containers.
`WORK_RUNTIME_DNS` is the supported Docker egress-policy hook: point it at a
filtering resolver to apply name-based allow/deny lists. Entries that are not
IPv4/IPv6 addresses are rejected and logged. DNS filtering does not constrain
direct-IP egress; add host firewall rules when a deployment requires that. The
Kubernetes backend instead uses the chart's default-deny NetworkPolicies and
`work.networkPolicy.blockedEgressCidrs` values.

On Docker, the interactive terminal and system diagnostics talk to the Docker
Engine API directly. They follow `WORK_DOCKER_SOCKET` when set, otherwise `DOCKER_HOST` —
either a `unix://` socket or a plain-HTTP `tcp://` endpoint such as a socket
proxy (see `docker-compose.socket-proxy.yml`) — otherwise
`/var/run/docker.sock`. A `DOCKER_HOST` this client cannot speak to (`ssh://`,
or `tcp://` with `DOCKER_TLS_VERIFY` set) reports the terminal and Docker
diagnostics as unavailable; the rest of Work continues to run through the
Docker CLI, which understands those endpoints on its own. On Kubernetes, the
terminal uses the Pod exec subresource and does not use a Docker endpoint.

Work reads these values when the backend starts. The preview port is internal to
the task container; Libre WebUI publishes it to a dynamically assigned loopback
port rather than exposing this value directly on every host interface.

Keep the runtime image pinned to a reviewed version or digest. Increasing
concurrency or resource limits raises the amount of runtime capacity one or
more autonomous runs can consume. `WORK_MAX_AGENT_ROUNDS` applies equally to
Ollama and plugin-backed runs; there is no lower plugin-only clamp. The
tool-call safety budget is `max(128, WORK_MAX_AGENT_ROUNDS × 8)`. When a run
uses its round budget, Work requests a final no-tools handoff from the model
and ends in the terminal `needs_input` state instead of returning a raw
round-limit error or claiming successful completion. A follow-up run continues
in the same durable workspace. Persisted tool output has a separate bound of
approximately 20,000 source characters plus a truncation marker.

These variables tune a Work runtime that is already reachable. Repository
single-instance Compose deployments enable it by default: the image ships the
Docker CLI and those Compose files mount the host Docker socket. Two
Compose-level variables control that wiring:

| Variable        | Default                | Purpose                                                         |
| --------------- | ---------------------- | --------------------------------------------------------------- |
| `DOCKER_GID`    | `0`                    | Group id of the host Docker socket, added to the container user |
| `DOCKER_SOCKET` | `/var/run/docker.sock` | Host path of the Docker socket to mount                         |

`DOCKER_GID` must be the socket's group **as seen inside a container**; a macOS
host reports a different value. The team Compose base mounts no socket and
keeps Docker-backed Work unavailable until
`docker-compose.team.work.yml` is added. That production overlay gives both
the application and worker the same internal filtered proxy endpoint, never a
socket mount or socket group. The proxy allows only the Docker API sections the
runtime uses, but container creation remains a Docker-host control credential;
use a dedicated or rootless Work daemon for a stronger boundary. The Helm chart
never mounts a node runtime socket. Enable its native Pod/PVC Work backend with
`work.enabled=true`.

The `solo` profile must remain at zero or one application replica because it
uses SQLite, local files, and process-local coordination. The Helm chart accepts
zero for a deliberate suspension and rejects larger solo replica counts or
solo autoscaling. A complete `team` profile may use multiple application
replicas and an external worker because PostgreSQL, S3, PGVector, and Redis own
the shared state. Work sandbox Pods scale independently in either profile; in
team mode the external worker receives the same Kubernetes runtime image,
StorageClass, and `work.env` limits as the application pods.

Repository Compose files also accept `WEBUI_BIND_ADDRESS` (default
`127.0.0.1`) and `WEBUI_PORT` (default `8080`). Keep the loopback default unless
a trusted LAN or host reverse proxy must reach the port.

## Provider Model Discovery

A provider's model catalog is rediscovered on its own when it is missing or
stale, so a reload reflects the models the provider currently serves. These
variables tune that cycle:

| Variable                                     | Default           | Purpose                                                                 |
| -------------------------------------------- | ----------------- | ----------------------------------------------------------------------- |
| `PLUGIN_MODEL_DISCOVERY_TTL_MS`              | `21600000` (6 h)  | Age at which a stored catalog is refreshed on the next plugin-list read |
| `PLUGIN_MODEL_DISCOVERY_RETRY_MS`            | `600000` (10 min) | Minimum gap between attempts, so a failing provider is not probed often |
| `PLUGIN_MODEL_DISCOVERY_REFRESH_DEADLINE_MS` | `3000`            | How long a plugin-list response waits for refreshes before answering    |

A refresh that outruns the deadline still completes and is served on the next
request. An explicit **Refresh models** always contacts the provider and
ignores the interval.

## Provider Plugin Keys

Provider plugins can use environment keys as deployment-wide defaults:

| Variable              | Provider                                    |
| --------------------- | ------------------------------------------- |
| `OPENAI_API_KEY`      | OpenAI and OpenAI TTS                       |
| `ANTHROPIC_API_KEY`   | Anthropic                                   |
| `GROQ_API_KEY`        | Groq                                        |
| `GEMINI_API_KEY`      | Google Gemini                               |
| `MISTRAL_API_KEY`     | Mistral                                     |
| `OPENROUTER_API_KEY`  | OpenRouter                                  |
| `KIMI_API_KEY`        | Kimi Code by Moonshot AI                    |
| `GITHUB_API_KEY`      | GitHub Models                               |
| `HUGGINGFACE_API_KEY` | Hugging Face APIs where configured          |
| `ELEVENLABS_API_KEY`  | ElevenLabs TTS                              |
| `COMFYUI_API_KEY`     | ComfyUI deployments that require an API key |

Users can also store provider credentials in the UI when per-user keys are
preferred. Environment keys are used only with the routing and authentication
projection of an unshadowed bundled definition. Imported definitions, writable
definitions that reuse a bundled ID, and administrator-saved custom routes
require a credential stored by that account. Libre WebUI will not attach an
environment key to those routes or expose it through discovery and availability
checks. Trust comes from a compiled hash of each shipped manifest, so container
layouts where the legacy and bundled plugin directories share a path remain
supported without treating a modified manifest as bundled.

User-saved keys are bound to the effective provider definition, source,
authentication contract, and routing values. Users must save a key again after
an administrator changes that destination. Pre-upgrade unbound keys are
accepted and bound on first use only for an exact shipped definition using its
bundled route.

For source launches, relative `PLUGINS_DIR` values resolve from the backend
directory. The packaged launcher instead converts an explicitly configured
relative value to a caller-absolute path before starting the backend. For
compatibility, Libre also reads the deterministic `backend/plugins` directory
and historical locations selected by previous configurations. Move those
definitions into `$DATA_DIR/plugins`; recovery reports legacy paths as external
state and blocks a volume-only snapshot while custom definitions remain there.
Plugin directories and JSON definitions must be physical regular entries—Libre
does not follow plugin symlinks.

## Frontend

| Variable             | Default                                 | Purpose                                              |
| -------------------- | --------------------------------------- | ---------------------------------------------------- |
| `VITE_API_BASE_URL`  | inferred from host/dev config           | Frontend API base URL                                |
| `VITE_WS_BASE_URL`   | inferred from API URL                   | Absolute `ws:`/`wss:` base for Chat and Work sockets |
| `VITE_APP_VERSION`   | package version injected by Vite config | Displayed app version                                |
| `VITE_DEMO_MODE`     | `false`                                 | Enables demo-mode mocks when `true`                  |
| `VITE_API_TIMEOUT`   | `300000`                                | Frontend API timeout in milliseconds                 |
| `VITE_BACKEND_URL`   | `http://localhost:3001`                 | Used by some auth helper components                  |
| `VITE_DEBUG_VERBOSE` | unset                                   | Enables verbose frontend debug logs in development   |
| `VITE_LOG_LEVEL`     | unset                                   | Overrides the frontend log level                     |
| `ELECTRON_BUILD`     | unset                                   | Enables Electron-specific Vite behavior when `true`  |

`VITE_WS_BASE_URL` overrides every WebSocket fallback for both Chat and the
Work terminal. It may include a reverse-proxy path prefix, but it must be an
absolute `ws:` or `wss:` URL without credentials, a query, or a fragment. When
it is unset, Electron `file:` clients use `ws://localhost:3001`; browser
clients derive their base from `VITE_API_BASE_URL`, then the production browser
origin or the development backend on port 3001.

## Maintenance Scripts

| Variable                  | Purpose                                             |
| ------------------------- | --------------------------------------------------- |
| `CHANGELOG_AI`            | Set to `0` to disable AI-assisted changelog drafts  |
| `CHANGELOG_AI_MODEL`      | Ollama model for release/changelog generation       |
| `CHANGELOG_AI_TIMEOUT_MS` | Timeout for AI changelog generation in milliseconds |

Example:

```bash
CHANGELOG_AI_MODEL=glm-5.2:cloud npm run changelog
CHANGELOG_AI=0 npm run release:minor
```

## Production Example

```env
NODE_ENV=production
PORT=3001
SERVE_FRONTEND=true
DATA_DIR=/data/libre-webui
CORS_ORIGIN=https://librewebui.example
BASE_URL=https://librewebui.example

JWT_SECRET=replace-with-a-long-random-secret
ENCRYPTION_KEY=replace-with-64-hex-characters
ENABLE_SIGNUP=false

OLLAMA_BASE_URL=http://ollama:11434
OLLAMA_TIMEOUT=300000
OLLAMA_LONG_OPERATION_TIMEOUT=900000
OLLAMA_MAX_CONTEXT=32768

TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...
TURNSTILE_EXPECTED_HOSTNAME=librewebui.example
```

## Related Docs

- [Authentication](./AUTHENTICATION)
- [Single Sign-On](./SINGLE_SIGN_ON)
- [Database Encryption](./DATABASE_ENCRYPTION)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker](./DOCKER)

---

# Qwen3-TTS Integration

URL: https://docs.librewebui.org/qwen3-tts

# Qwen3-TTS Integration

Run Alibaba's Qwen3-TTS locally for high-quality, multilingual text-to-speech. This guide covers setting up the OpenAI-compatible TTS server included with Libre WebUI.

## Overview

Qwen3-TTS is an advanced text-to-speech system featuring:

- **9 pre-built voices** spanning English, Chinese, Japanese, and Korean
- **10 language support** including German, French, Spanish, Italian, Portuguese, and Russian
- **Voice cloning** from 3-second audio samples
- **Voice design** using natural language descriptions
- **Instruction control** for emotion and prosody

The included server wraps Qwen3-TTS in an OpenAI-compatible API, allowing Libre WebUI to use it through the standard plugin system.

## Requirements

| Component    | Minimum           | Recommended        |
| ------------ | ----------------- | ------------------ |
| **Python**   | 3.12+             | 3.12 (not 3.14)    |
| **GPU VRAM** | 4GB (0.6B models) | 8GB+ (1.7B models) |
| **RAM**      | 8GB               | 16GB+              |
| **Disk**     | 5GB               | 10GB               |

### Platform Support

| Platform          | Backend | Notes                                 |
| ----------------- | ------- | ------------------------------------- |
| **NVIDIA GPU**    | CUDA    | Best performance, bfloat16 support    |
| **Apple Silicon** | MPS     | Use 0.6B models for memory efficiency |
| **CPU**           | PyTorch | Slower, use 0.6B models               |

:::tip Apple Silicon Users
Use the `customvoice-0.6b` model variant on Mac to avoid memory pressure. The 1.7B models may cause system instability on machines with 16GB unified memory.
:::

## Quick Start

### 1. Install the Server

```bash
cd examples/qwen-tts-server

# Create virtual environment (Python 3.12 required)
python3.12 -m venv venv
source venv/bin/activate  # Linux/macOS
# or: venv\Scripts\activate  # Windows

# Install dependencies
pip install -r requirements.txt
```

### 2. Start the Server

```bash
# NVIDIA GPU (recommended)
python server.py --model customvoice-1.7b

# Apple Silicon
python server.py --model customvoice-0.6b

# CPU (slower)
python server.py --model customvoice-0.6b
```

The server runs at `http://localhost:8100` by default.

### 3. Configure Libre WebUI

The plugin is pre-configured in `plugins/qwen-tts.json`. Enable it in **Settings → Plugins → Qwen3 TTS**.

### 4. Test It

```bash
curl http://localhost:8100/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model": "qwen3-tts", "input": "Hello, welcome to Libre WebUI!", "voice": "Ryan"}' \
  --output speech.wav
```

## Available Models

| Model              | Size   | Use Case                                  |
| ------------------ | ------ | ----------------------------------------- |
| `customvoice-1.7b` | ~3.5GB | Pre-built voices with instruction control |
| `customvoice-0.6b` | ~1.5GB | Lightweight variant for limited VRAM      |
| `voicedesign-1.7b` | ~3.5GB | Create voices from text descriptions      |
| `base-1.7b`        | ~3.5GB | Voice cloning from 3-second samples       |
| `base-0.6b`        | ~1.5GB | Lightweight voice cloning                 |

## Voices

### Pre-Built Voices (CustomVoice Models)

| Voice        | Language | Description             |
| ------------ | -------- | ----------------------- |
| **Ryan**     | English  | Male, clear and natural |
| **Aiden**    | English  | Male, warm tone         |
| **Vivian**   | Chinese  | Female, professional    |
| **Serena**   | Chinese  | Female, friendly        |
| **Uncle_Fu** | Chinese  | Male, mature            |
| **Dylan**    | Chinese  | Male, Beijing dialect   |
| **Eric**     | Chinese  | Male, Sichuan dialect   |
| **Ono_Anna** | Japanese | Female                  |
| **Sohee**    | Korean   | Female                  |

### OpenAI Voice Aliases

For compatibility with OpenAI TTS clients, the server maps OpenAI voice names:

| OpenAI Voice | Maps To  |
| ------------ | -------- |
| `alloy`      | Ryan     |
| `echo`       | Aiden    |
| `fable`      | Vivian   |
| `onyx`       | Uncle_Fu |
| `nova`       | Serena   |
| `shimmer`    | Ono_Anna |

## API Reference

### Speech Generation

**Endpoint:** `POST /v1/audio/speech`

```json
{
  "model": "qwen3-tts",
  "input": "Text to convert to speech",
  "voice": "Ryan",
  "response_format": "wav",
  "instruct": "Speak with enthusiasm",
  "language": "English"
}
```

| Parameter         | Type   | Default     | Description                           |
| ----------------- | ------ | ----------- | ------------------------------------- |
| `model`           | string | `qwen3-tts` | Model identifier                      |
| `input`           | string | required    | Text to synthesize (max 10,000 chars) |
| `voice`           | string | `ryan`      | Voice name (see table above)          |
| `response_format` | string | `wav`       | Audio format (only `wav` supported)   |
| `instruct`        | string | `""`        | Emotion/prosody instruction           |
| `language`        | string | auto-detect | Override language detection           |

**Response:** Audio file (`audio/wav`)

### Voice Design

**Endpoint:** `POST /v1/audio/voice-design`

Create custom voices from natural language descriptions.

```json
{
  "model": "qwen3-tts-voicedesign",
  "input": "Welcome to our service.",
  "voice_description": "A warm, friendly female voice with a slight British accent",
  "language": "English"
}
```

:::note
Requires the `voicedesign-1.7b` model to be loaded.
:::

### Voice Cloning

**Endpoint:** `POST /v1/audio/voice-clone`

Clone a voice from a 3+ second audio sample.

```bash
curl -X POST http://localhost:8100/v1/audio/voice-clone \
  -F "input=Hello, this is my cloned voice." \
  -F "reference_audio=@reference.wav" \
  -F "reference_text=This is what was said in the reference." \
  --output cloned.wav
```

| Parameter         | Type   | Description                   |
| ----------------- | ------ | ----------------------------- |
| `input`           | string | Text to synthesize            |
| `reference_audio` | file   | 3+ second audio sample        |
| `reference_text`  | string | Transcript of reference audio |

:::note
Requires the `base-1.7b` or `base-0.6b` model to be loaded.
:::

### List Voices

**Endpoint:** `GET /v1/voices`

```json
{
  "voices": [
    {"id": "ryan", "name": "Ryan", "language": "English"},
    {"id": "aiden", "name": "Aiden", "language": "English"},
    ...
  ]
}
```

### Health Check

**Endpoint:** `GET /health`

```json
{ "status": "healthy", "model_loaded": true }
```

## Server Configuration

```bash
python server.py [OPTIONS]
```

| Option    | Default            | Description           |
| --------- | ------------------ | --------------------- |
| `--host`  | `0.0.0.0`          | Host to bind to       |
| `--port`  | `8100`             | Port to bind to       |
| `--model` | `customvoice-1.7b` | Model variant to load |

### Network Access

To access the server from other machines on your network:

```bash
# Start server on all interfaces
python server.py --host 0.0.0.0 --port 8100

# Access from another machine
curl http://192.168.1.100:8100/v1/audio/speech ...
```

Update the plugin endpoint in `plugins/qwen-tts.json`:

```json
{
  "endpoint": "http://192.168.1.100:8100/v1/audio/speech",
  "capabilities": {
    "tts": {
      "endpoint": "http://192.168.1.100:8100/v1/audio/speech"
    }
  }
}
```

## Production Features

### Text Sanitization

The server automatically sanitizes input text to prevent model hangs:

- Removes emojis and symbols
- Strips markdown formatting (`*bold*`, `_italic_`, etc.)
- Collapses repeated characters (`FUUUUU` → `FUU`)
- Removes stage directions (`*(action)*`, `(whispers)`)
- Normalizes whitespace

### Text Chunking

Long text is automatically split at sentence boundaries:

- Maximum 500 characters per chunk
- 30-second timeout per chunk
- Failed chunks are skipped, remaining chunks continue
- Chunks are concatenated into single audio response

This prevents timeouts on long AI responses while maintaining natural speech flow.

## Multi-GPU Setup

For systems with multiple GPUs, the server forces single-GPU execution to avoid tensor device mismatches:

```python
device_map = {"": "cuda:0"}  # Uses first GPU only
```

To use a specific GPU:

```bash
CUDA_VISIBLE_DEVICES=1 python server.py --model customvoice-1.7b
```

## Troubleshooting

### Model Download Fails

The model downloads from Hugging Face on first run. If it fails:

```bash
# Set Hugging Face token for gated models
export HF_TOKEN=hf_...

# Or download manually
huggingface-cli download Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice
```

### Out of Memory (Apple Silicon)

```
RuntimeError: MPS backend out of memory
```

Use the smaller model variant:

```bash
python server.py --model customvoice-0.6b
```

### CUDA Out of Memory

```
torch.cuda.OutOfMemoryError: CUDA out of memory
```

1. Close other GPU applications
2. Use the 0.6B model variant
3. Reduce chunk size in server.py (`max_chunk_size=300`)

### Server Times Out

If generation times out on long text:

1. The server automatically chunks text and continues with remaining chunks
2. Check server logs for which chunks timed out
3. Consider shortening your input text

### Audio Sounds Wrong

- **Repeated syllables:** Usually caused by emojis or special characters. The sanitizer should handle this automatically.
- **Wrong language:** Set the `language` parameter explicitly in the request.
- **Unnatural pauses:** Text may be splitting at wrong boundaries. Check for unusual punctuation.

## Plugin Configuration

The included plugin (`plugins/qwen-tts.json`):

```json
{
  "id": "qwen-tts",
  "name": "Qwen3 TTS",
  "type": "tts",
  "endpoint": "http://localhost:8100/v1/audio/speech",
  "auth": {
    "header": "",
    "key_env": ""
  },
  "model_map": [
    "qwen3-tts",
    "qwen3-tts-customvoice",
    "qwen3-tts-voicedesign",
    "qwen3-tts-clone"
  ],
  "capabilities": {
    "tts": {
      "endpoint": "http://localhost:8100/v1/audio/speech",
      "model_map": [
        "qwen3-tts",
        "qwen3-tts-customvoice",
        "qwen3-tts-voicedesign",
        "qwen3-tts-clone"
      ],
      "config": {
        "voices": [
          "Ryan",
          "Aiden",
          "Vivian",
          "Serena",
          "Uncle_Fu",
          "Dylan",
          "Eric",
          "Ono_Anna",
          "Sohee"
        ],
        "default_voice": "Ryan",
        "formats": ["wav"],
        "default_format": "wav",
        "max_characters": 10000,
        "supports_streaming": false,
        "no_auth_required": true
      }
    }
  },
  "description": "Qwen3-TTS local TTS server (NVIDIA CUDA, Apple MPS, or CPU)",
  "documentation_url": "https://github.com/QwenLM/Qwen3-TTS"
}
```

## Resources

- [Qwen3-TTS GitHub](https://github.com/QwenLM/Qwen3-TTS) - Official repository
- [Qwen3-TTS Demo](https://huggingface.co/spaces/Qwen/Qwen3-TTS-Demo) - Try it online
- [Alibaba Cloud TTS Docs](https://www.alibabacloud.com/help/en/model-studio/qwen-tts) - Cloud API documentation

---

# Kyutai TTS Integration

URL: https://docs.librewebui.org/kyutai-tts

# Kyutai TTS Integration

Run Kyutai's TTS models locally for high-quality text-to-speech. This guide covers both Pocket TTS (CPU) and TTS 1.6B (GPU) with OpenAI-compatible servers included in Libre WebUI.

## Overview

Kyutai offers two TTS models:

| Model          | Parameters | Device      | Best For                           |
| -------------- | ---------- | ----------- | ---------------------------------- |
| **Pocket TTS** | 100M       | CPU only    | Laptops, low-resource environments |
| **TTS 1.6B**   | 1.6B       | GPU/MPS/CPU | Servers, high-quality synthesis    |

Both use the CALM (Continuous Audio Language Models) framework and support voice cloning from audio samples.

## Pocket TTS (CPU)

Lightweight TTS that runs in real-time on CPU. No GPU required.

### Requirements

| Component   | Minimum     |
| ----------- | ----------- |
| **Python**  | 3.10 - 3.14 |
| **PyTorch** | 2.5+        |
| **RAM**     | 4GB         |
| **Disk**    | 500MB       |

### Quick Start

```bash
cd examples/kyutai-tts-server

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Start server
python server.py
```

Server runs at `http://localhost:8200`.

### Test It

```bash
curl http://localhost:8200/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model": "kyutai-tts", "input": "Hello, welcome to Libre WebUI!", "voice": "alba"}' \
  --output speech.wav
```

### Voices

| Voice       | Description               |
| ----------- | ------------------------- |
| **Alba**    | Female, clear and natural |
| **Marius**  | Male, warm tone           |
| **Javert**  | Male, authoritative       |
| **Jean**    | Male, gentle              |
| **Fantine** | Female, soft              |
| **Cosette** | Female, young             |
| **Eponine** | Female, expressive        |
| **Azelma**  | Female, bright            |

### Performance

- ~6x real-time on MacBook Air M4
- ~200ms latency for first audio chunk
- Uses only 2 CPU cores

---

## TTS 1.6B (GPU)

High-quality TTS with GPU acceleration. Automatic device selection: CUDA > MPS > CPU.

### Requirements

| Component    | Minimum | Recommended |
| ------------ | ------- | ----------- |
| **Python**   | 3.10+   | 3.12        |
| **GPU VRAM** | 6GB     | 8GB+        |
| **RAM**      | 8GB     | 16GB+       |
| **Disk**     | 4GB     | 8GB         |

### Platform Support

| Platform          | Backend | Notes                              |
| ----------------- | ------- | ---------------------------------- |
| **NVIDIA GPU**    | CUDA    | Best performance, bfloat16 support |
| **Apple Silicon** | MPS     | Uses float16                       |
| **CPU**           | PyTorch | Slower, float32                    |

### Quick Start

```bash
cd examples/kyutai-tts-1.6b-server

# Create virtual environment
python3 -m venv venv
source venv/bin/activate

# Install PyTorch with CUDA (for NVIDIA GPUs)
pip install torch --index-url https://download.pytorch.org/whl/cu121

# Install dependencies
pip install -r requirements.txt

# Start server (auto-detects GPU)
python server.py
```

Server runs at `http://localhost:8201`.

### Device Selection

```bash
# Auto-detect (CUDA > MPS > CPU)
python server.py

# Force specific device
python server.py --device cuda
python server.py --device mps
python server.py --device cpu
```

### Test It

```bash
curl http://localhost:8201/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model": "kyutai-tts-1.6b", "input": "Hello from the GPU!", "voice": "alba"}' \
  --output speech.wav
```

### Voices

**Alba MacKenna (CC BY 4.0):**

| Voice                  | Style               |
| ---------------------- | ------------------- |
| `alba` / `alba-casual` | Casual conversation |
| `alba-merchant`        | Merchant character  |
| `alba-announcer`       | Announcer style     |

**Expresso (CC BY-NC 4.0 - non-commercial):**

| Voice            | Emotion |
| ---------------- | ------- |
| `expresso-happy` | Happy   |
| `expresso-sad`   | Sad     |
| `expresso-angry` | Angry   |

**VCTK (CC BY 4.0):**

- `vctk-p225`, `vctk-p226`, `vctk-p227`, `vctk-p228`

---

## Voice Cloning

Both servers support cloning voices from audio files.

### Pocket TTS

```bash
# From local file
curl http://localhost:8200/v1/audio/voice-clone \
  -F "input=Hello from a cloned voice" \
  -F "reference_audio=@my_voice.wav" \
  --output cloned.wav

# From HuggingFace URL
curl http://localhost:8200/v1/audio/voice-clone-url \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Hello world!",
    "voice_url": "hf://kyutai/tts-voices/alba-mackenna/casual.wav"
  }' \
  --output speech.wav
```

### TTS 1.6B

Pass any HuggingFace voice path as the `voice` parameter:

```bash
curl http://localhost:8201/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kyutai-tts-1.6b",
    "input": "Custom voice synthesis",
    "voice": "hf://kyutai/tts-voices/vctk/p230.wav"
  }' \
  --output speech.wav
```

---

## API Reference

### Speech Generation

**Endpoint:** `POST /v1/audio/speech`

```json
{
  "model": "kyutai-tts",
  "input": "Text to convert to speech",
  "voice": "alba",
  "response_format": "wav",
  "stream": false
}
```

| Parameter         | Type    | Default  | Description                           |
| ----------------- | ------- | -------- | ------------------------------------- |
| `model`           | string  | varies   | `kyutai-tts` or `kyutai-tts-1.6b`     |
| `input`           | string  | required | Text to synthesize (max 10,000 chars) |
| `voice`           | string  | `alba`   | Voice name or HuggingFace path        |
| `response_format` | string  | `wav`    | Audio format (only `wav` supported)   |
| `stream`          | boolean | `false`  | Enable streaming (Pocket TTS only)    |
| `cfg_coef`        | float   | `2.0`    | Classifier-free guidance (1.6B only)  |

**Response:** Audio file (`audio/wav`)

### OpenAI Voice Aliases

For compatibility with OpenAI TTS clients:

| OpenAI Voice | Pocket TTS | TTS 1.6B       |
| ------------ | ---------- | -------------- |
| `alloy`      | alba       | alba           |
| `echo`       | marius     | vctk-p225      |
| `fable`      | cosette    | expresso-happy |
| `onyx`       | javert     | vctk-p226      |
| `nova`       | fantine    | alba-announcer |
| `shimmer`    | eponine    | alba-merchant  |

### List Voices

**Endpoint:** `GET /v1/voices`

### Health Check

**Endpoint:** `GET /health`

---

## Plugin Configuration

### Pocket TTS

Enable in **Settings > Plugins > Kyutai TTS**

Plugin file: `plugins/kyutai-tts.json`

```json
{
  "id": "kyutai-tts",
  "name": "Kyutai TTS",
  "type": "tts",
  "endpoint": "http://localhost:8200/v1/audio/speech",
  "capabilities": {
    "tts": {
      "config": {
        "voices": [
          "Alba",
          "Marius",
          "Javert",
          "Jean",
          "Fantine",
          "Cosette",
          "Eponine",
          "Azelma"
        ],
        "default_voice": "Alba",
        "supports_streaming": true,
        "no_auth_required": true
      }
    }
  }
}
```

### TTS 1.6B

Enable in **Settings > Plugins > Kyutai TTS 1.6B**

Plugin file: `plugins/kyutai-tts-1.6b.json`

```json
{
  "id": "kyutai-tts-1.6b",
  "name": "Kyutai TTS 1.6B",
  "type": "tts",
  "endpoint": "http://localhost:8201/v1/audio/speech",
  "capabilities": {
    "tts": {
      "config": {
        "voices": [
          "Alba",
          "Alba-Casual",
          "Alba-Merchant",
          "Alba-Announcer",
          "Expresso-Happy",
          "Expresso-Sad",
          "Expresso-Angry",
          "VCTK-P225",
          "VCTK-P226"
        ],
        "default_voice": "Alba",
        "supports_streaming": true,
        "no_auth_required": true
      }
    }
  }
}
```

---

## Network Access

To access from other machines:

```bash
# Start server on all interfaces
python server.py --host 0.0.0.0

# Access from another machine
curl http://192.168.1.100:8200/v1/audio/speech ...
```

Update the plugin endpoint accordingly:

```json
{
  "endpoint": "http://192.168.1.100:8200/v1/audio/speech"
}
```

---

## Troubleshooting

### Model Download Fails

Models download from HuggingFace on first run:

```bash
# Set token for gated models
export HF_TOKEN=hf_...
```

### CUDA Out of Memory

For TTS 1.6B on limited VRAM:

1. Close other GPU applications
2. Try `cfg_coef=1.5` for lower memory usage
3. Use Pocket TTS instead (CPU-based)

### Audio Quality Issues

- **Robotic sound:** Try a different voice
- **Cut off audio:** Text may be too long, server chunks automatically
- **Wrong pronunciation:** Model is optimized for English and French

### MPS (Apple Silicon) Issues

```
RuntimeError: MPS backend error
```

The 1.6B model uses float16 on MPS. If issues persist, force CPU:

```bash
python server.py --device cpu
```

---

## Comparison with Qwen3-TTS

| Feature       | Kyutai Pocket | Kyutai 1.6B | Qwen3-TTS    |
| ------------- | ------------- | ----------- | ------------ |
| Parameters    | 100M          | 1.6B        | 0.6B-1.7B    |
| GPU Required  | No            | Optional    | Yes          |
| Languages     | English       | EN/FR       | 10 languages |
| Voice Cloning | Yes           | Yes         | Yes          |
| Voice Design  | No            | No          | Yes          |
| Port          | 8200          | 8201        | 8100         |

Choose Kyutai for English-focused use cases with simpler setup. Choose Qwen3-TTS for multilingual support and voice design features.

---

## Resources

- [Kyutai TTS](https://kyutai.org/tts) - Official project page
- [Pocket TTS GitHub](https://github.com/kyutai-labs/pocket-tts) - CPU model
- [Delayed Streams Modeling](https://github.com/kyutai-labs/delayed-streams-modeling) - 1.6B model
- [Voice Collection](https://huggingface.co/kyutai/tts-voices) - Available voices
- [Model Card](https://huggingface.co/kyutai/tts-1.6b-en_fr) - Technical details

---

# Hugging Face Hub

URL: https://docs.librewebui.org/huggingface-hub

# Hugging Face Hub

Libre WebUI integrates with Hugging Face in two ways:

- Provider plugin access through Hugging Face Inference Providers.
- In-app Hub browsing for compatible models, including GGUF models that can be pulled through Ollama.

## Configure the Provider Plugin

Create a Hugging Face access token with inference access, then add it in Settings or as an environment variable:

```env
HUGGINGFACE_API_KEY=hf_...
```

Enable the Hugging Face plugin in **Settings > Plugins**.

## Capabilities

Depending on model and provider availability, Hugging Face can provide:

- Chat/completion models
- Text-to-speech
- Image generation
- Embeddings
- Speech-to-text

Hugging Face availability, pricing, and routing can change by model. Check the model page and Hugging Face provider UI for current status.

## Model Browser

The in-app browser lets you:

- Search by model name or author.
- Filter by task.
- Sort by trending, downloads, likes, creation date, or update date.
- Inspect gated model status.
- Pull compatible GGUF files through Ollama when supported.

The browser fetches Hub metadata through the backend proxy and caches results for performance.
The backend requires an active Libre session for Hub discovery, and only a
current administrator can clear the shared model cache.

## GGUF and Ollama

For local inference, look for GGUF-format models. Libre WebUI can use Ollama-compatible `hf.co/...` model references when a repository exposes suitable GGUF files.

Example pattern:

```bash
ollama run hf.co/owner/model-repo:tag
```

Use the UI when possible; it reduces copy/paste mistakes and keeps model metadata visible.

## Inference Provider Routing

Hugging Face's OpenAI-compatible router supports model IDs from the Hub. Some routes support suffixes or provider preferences. Because this changes over time, prefer current Hugging Face docs and the in-app model browser for exact model IDs.

## Gated Models

Some models require accepting terms before use:

1. Open the model page on Hugging Face.
2. Accept the license or terms.
3. Confirm your token has access.
4. Retry from Libre WebUI.

Gated models can appear in discovery before your token is allowed to run them.

## Troubleshooting

**Unauthorized**

- Check the token value.
- Confirm the token has inference permissions.
- If using per-user credentials, confirm the key is saved for the current user.

**Model unavailable**

- Check the Hugging Face model page.
- Try another provider route if available.
- Confirm you accepted gated terms.

**GGUF pull fails**

- Confirm the repository contains GGUF files.
- Try the exact `hf.co/...` reference in a terminal with Ollama.
- Use a smaller quantization if disk or memory is limited.

## Related Docs

- [Plugin Architecture](./PLUGIN_ARCHITECTURE)
- [Working with Models](./WORKING_WITH_MODELS)
- [Document Chat](./RAG_FEATURE)

---

# Local GPU Stack

URL: https://docs.librewebui.org/local-gpu-stack

# Local GPU Stack

This guide describes the repository-supported local GPU setup: Ollama runs natively on the host for direct GPU access, and Libre WebUI runs in Docker with `docker-compose.dev.external-ollama.yml`.

## Work Availability

Ollama runs natively on the host GPU. Work is enabled through the mounted host
Docker socket, letting Libre WebUI create task-scoped containers on the same
daemon. No native Libre WebUI install is needed.

The socket grants root-equivalent control of the Docker host. Remove its mount
unless Work is needed, and on Linux set `DOCKER_GID` in `.env`.

On memory-constrained machines, Work can instead use an Ollama Cloud model or a
configured remote completion/chat plugin. That reduces local model memory
pressure, but Docker is still required for the task workspace and commands.

## What You Get

- Libre WebUI using the `librewebui/libre-webui:dev` image.
- Native host Ollama for local model inference.
- Persistent Libre WebUI data volumes.
- A clean path for testing the dev image without containerizing Ollama.

## Prerequisites

| Component | Requirement                                                                   |
| --------- | ----------------------------------------------------------------------------- |
| Docker    | Docker Desktop or Docker Engine with Compose v2                               |
| Ollama    | Installed and running on the host                                             |
| GPU       | Optional but recommended for local models                                     |
| NVIDIA    | Current driver and NVIDIA Container Toolkit if you containerize GPU workloads |
| Disk      | Enough space for Ollama model files                                           |

## Install and Verify Ollama

Linux:

```bash
curl -fsSL https://ollama.com/install.sh | sh
```

macOS:

```bash
brew install ollama
ollama serve
```

Pull a starter model:

```bash
ollama pull gemma3:4b
```

Verify the API:

```bash
curl http://localhost:11434/api/version
```

## Start Libre WebUI

From the repository root:

```bash
docker compose -f docker-compose.dev.external-ollama.yml up -d
```

Open [http://localhost:8080](http://localhost:8080).

## Custom Ollama Endpoint

If Ollama is on another machine:

```bash
OLLAMA_BASE_URL=http://192.168.1.100:11434 docker compose -f docker-compose.dev.external-ollama.yml up -d
```

For Tailscale:

```bash
OLLAMA_BASE_URL=http://100.x.y.z:11434 docker compose -f docker-compose.dev.external-ollama.yml up -d
```

## Recommended Models

For a first GPU workstation:

```bash
ollama pull gemma3:4b
ollama pull qwen3:8b
ollama pull deepseek-r1:8b
ollama pull nomic-embed-text
```

Use the Model Manager to discover larger models once the basic stack is working.

## LAN Access

Add your LAN or Tailscale origin to `CORS_ORIGIN` in the compose environment before exposing the app to another device:

```env
CORS_ORIGIN=http://localhost:8080,http://192.168.1.50:8080
```

Then restart:

```bash
docker compose -f docker-compose.dev.external-ollama.yml up -d
```

## Useful Commands

```bash
docker compose -f docker-compose.dev.external-ollama.yml ps
docker compose -f docker-compose.dev.external-ollama.yml logs -f libre-webui
docker compose -f docker-compose.dev.external-ollama.yml pull
docker compose -f docker-compose.dev.external-ollama.yml up -d
```

## Troubleshooting

**Container cannot reach Ollama**

```bash
docker compose -f docker-compose.dev.external-ollama.yml exec libre-webui \
  wget -O- http://host.docker.internal:11434/api/version
```

On Linux, add:

```yaml
extra_hosts:
  - 'host.docker.internal:host-gateway'
```

**Model runs on CPU**

Native Ollama uses the host installation. Check your Ollama install, GPU driver, and `ollama ps`.

**Need TTS**

Use the [Qwen3-TTS](./QWEN3_TTS), [Kyutai TTS](./KYUTAI_TTS), or
[LongCat AudioDiT](./LONGCAT_AUDIODIT) server guide, then enable the matching
plugin in Settings.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Docker with External Ollama](./DOCKER_EXTERNAL_OLLAMA)
- [Hardware Requirements](./HARDWARE_REQUIREMENTS)
- [Working with Models](./WORKING_WITH_MODELS)

---

# Libre Claw Integration

URL: https://docs.librewebui.org/libre-claw-integration

# Libre Claw Integration

Libre WebUI includes a native integration for
[Libre Claw](https://github.com/kroonen-ai/libre-claw), Kroonen AI's local
agent harness. Libre Claw is exposed as a first-class agent surface inside
Libre WebUI rather than as a generic completion-provider plugin.

Libre Claw remains the runtime that owns the powerful agent behavior:

- durable runs and JSONL event logs
- file, shell, git, browser, HTTP, web search, MCP, and schedule tools
- approval requests before side effects
- persistent memory and project/user skills
- goal mode
- fallback model routes
- usage reports
- daemon-owned automations and Telegram delivery
- the standalone local dashboard

Libre WebUI talks to the Libre Claw daemon over HTTP and exposes those features
inside the normal authenticated WebUI shell.

In the interface this surface is called **Agents** — the icon in the sidebar,
the `/agents` route, and the command palette entry all use that name. Libre Claw
is the runtime behind it, so the daemon, its command, and its environment
variables keep their own name throughout this guide.

## Libre Claw And Work Are Different

Libre Claw remains an optional external runtime. It is not required for
Libre WebUI's built-in **Work** mode.

|                         | Work                                                                   | Libre Claw                                                                |
| ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Runtime                 | Native Libre WebUI model/tool loop plus Docker                         | Separate Libre Claw daemon                                                |
| Primary scope           | One persistent, isolated coding workspace per task                     | Broader agent runs, memory, approvals, schedules, browser, MCP, and tools |
| Persistence             | SQLite task history plus a task-owned Docker named volume              | Libre Claw's own run and memory stores                                    |
| Model routing           | Ollama, Ollama Cloud, or configured Libre WebUI completion/chat plugin | Libre Claw provider, model, and fallback configuration                    |
| Availability without it | Work continues normally                                                | Only the `/agents` Libre Claw surface is disconnected                     |

Use Work when a model should build or modify files inside a constrained
task-scoped container. Use Libre Claw when you need the daemon's broader tool
set, approval system, memory, goals, or automations.

## Architecture

```mermaid
flowchart LR
    UI["Libre WebUI<br/>/agents page"]
    API["Libre WebUI backend<br/>/api/libre-claw/*"]
    LC["Libre Claw daemon<br/>127.0.0.1:8766"]
    RUNS["~/.libre-claw/runs"]
    MEM["~/.libre-claw/memory.db"]
    TOOLS["Libre Claw tools<br/>shell, files, browser, git, MCP"]

    UI --> API
    API --> LC
    LC --> RUNS
    LC --> MEM
    LC --> TOOLS
```

The WebUI backend never runs shell commands or browser tools directly for Libre
Claw. It proxies authenticated admin requests to the local daemon and displays
the daemon's run state, events, approvals, automations, usage, and configuration.

## Start Libre Claw

Install Libre Claw:

```bash
git clone https://github.com/kroonen-ai/libre-claw.git
cd libre-claw
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

Start the daemon:

```bash
libre-claw start
```

The default dashboard and API are available at:

```text
http://127.0.0.1:8766/dashboard
```

Then open **Libre WebUI → Libre Claw**.

## Backend Environment

Libre WebUI uses the local daemon URL by default:

```bash
LIBRE_CLAW_BASE_URL=http://127.0.0.1:8766
LIBRE_CLAW_TIMEOUT_MS=30000
```

Set `LIBRE_CLAW_BASE_URL` if the daemon runs on another host, a Tailscale IP, or
a reverse-proxied local service.

## WebUI Routes

All Libre Claw routes require an authenticated admin user because they can reveal
local file paths and trigger agent work:

| WebUI route                                             | Libre Claw feature                     |
| ------------------------------------------------------- | -------------------------------------- |
| `GET /api/libre-claw/status`                            | Daemon health, base URL, dashboard URL |
| `GET /api/libre-claw/config/model`                      | Current provider/model                 |
| `PATCH /api/libre-claw/config/model`                    | Update provider/model                  |
| `GET /api/libre-claw/config/fallback`                   | Current fallback route                 |
| `PATCH /api/libre-claw/config/fallback`                 | Update fallback route                  |
| `PATCH /api/libre-claw/config/theme`                    | Update dashboard/TUI theme             |
| `GET /api/libre-claw/runs`                              | List durable runs                      |
| `POST /api/libre-claw/runs`                             | Start chat or goal-mode run            |
| `GET /api/libre-claw/runs/:id`                          | Run metadata and artifacts             |
| `GET /api/libre-claw/runs/:id/events`                   | Incremental run events                 |
| `POST /api/libre-claw/runs/:id/cancel`                  | Cancel a run                           |
| `POST /api/libre-claw/runs/:id/permissions/:toolCallId` | Approve or deny a tool call            |
| `GET /api/libre-claw/usage`                             | Usage summary and records              |
| `GET /api/libre-claw/automations`                       | List scheduled automations             |
| `POST /api/libre-claw/automations`                      | Create automation                      |
| `PATCH /api/libre-claw/automations/:id`                 | Update automation                      |
| `POST /api/libre-claw/automations/:id/run`              | Run automation now                     |
| `POST /api/libre-claw/automations/:id/pause`            | Pause automation                       |
| `POST /api/libre-claw/automations/:id/resume`           | Resume automation                      |
| `DELETE /api/libre-claw/automations/:id`                | Delete automation                      |

## Starting Runs

The WebUI page can start:

- **Chat runs** — normal Libre Claw agent turns.
- **Goal runs** — bounded autopilot mode where Libre Claw continues until its
  judge marks the goal complete or it reaches the configured limit.

Optional provider/model overrides are passed through to Libre Claw for that run.
If omitted, Libre Claw uses its configured default provider and model.

## Approvals

When Libre Claw asks for permission, WebUI shows the pending tool call and lets an
admin choose:

- `allow_once`
- `deny`
- `always_allow_tool`

Libre Claw still owns the permission model and safety checks. WebUI only records
the admin's choice through the daemon API.

## Automations

Libre WebUI can create and manage Libre Claw daemon automations:

- report automations
- Telegram-routed automations
- pause/resume
- run now
- delete

These are Libre Claw automations, not host cron jobs. Reports and execution
history remain in Libre Claw's run store.

## Memory, Skills, Soul, And Tools

Memory, skills, soul files, MCP tools, browser tools, SearXNG, Petdex, and
Telegram are configured in Libre Claw itself. WebUI displays and controls the
daemon-facing features without duplicating Libre Claw's internal configuration
system.

Useful Libre Claw commands:

```text
/memory status
/skills list
/soul status
/tools list
/workspace status
/telegram
/petdex status
```

## Troubleshooting

### WebUI Shows "Libre Claw daemon is not connected"

Start the daemon:

```bash
libre-claw start
```

Or set the backend URL:

```bash
LIBRE_CLAW_BASE_URL=http://127.0.0.1:8766
```

### Permission Buttons Do Nothing

The run may have already finished or the permission may have been resolved from
another surface, such as the Libre Claw dashboard or Telegram bridge. Refresh the
WebUI page and inspect the run timeline.

### A Tool Was Denied

Libre Claw's permission manager is authoritative. Check Libre Claw's own config
and run events if a call is denied before WebUI sees an approval request.

### Automations Do Not Run

Check Libre Claw daemon status and automation config:

```bash
libre-claw status
libre-claw start
```

The daemon must be running for scheduled automations and Telegram delivery.

## Related Docs

- [Work: Isolated Workspaces](./WORKSPACES)
- [Authentication](./AUTHENTICATION)
- [Environment Variables](./ENVIRONMENT_VARIABLES)

---

# Kimi Code

URL: https://docs.librewebui.org/kimi-code

# Kimi Code

Libre WebUI includes a bundled Kimi Code provider plugin for Moonshot AI's
OpenAI-compatible coding API.

## Configure the Provider

Create an API key in the
[Kimi Code Console](https://www.kimi.com/code/console), then save it for your
user in **Settings > Plugins > Kimi Code (Moonshot AI)** or provide it as a
deployment-wide environment variable:

```env
KIMI_API_KEY=...
```

Activate the plugin after saving the credential. Its models then appear in the
model selector.

## Available Models

| Model ID                    | Model                    | Availability                                             |
| --------------------------- | ------------------------ | -------------------------------------------------------- |
| `k3`                        | Kimi K3                  | Moderato and above; up to 1M context on eligible tiers   |
| `k3-256k`                   | Kimi K3                  | Moderato and above; fixed 256K context                   |
| `kimi-for-coding`           | Kimi K2.7 Code           | All Kimi Code members                                    |
| `kimi-for-coding-highspeed` | Kimi K2.7 Code HighSpeed | Allegretto and above; higher speed and greater quota use |

Kimi documents K3 as supporting up to a one-million-token context window for
eligible membership tiers. Libre WebUI leaves K3's reasoning effort unset so
the Kimi API applies its documented default, currently `high`.

Kimi Code models choose fixed sampling values for their active reasoning mode.
Libre WebUI therefore does not send the global temperature, top-p, frequency
penalty, or presence penalty settings to this provider. This follows Kimi's
recommendation and prevents invalid-parameter errors when its defaults change.

## API Endpoint

The bundled plugin uses Kimi Code's OpenAI-compatible endpoint:

```text
https://api.kimi.com/coding/v1/chat/completions
```

The endpoint can be overridden in the plugin variables for compatible gateways
or proxies. HTTP overrides are supported for self-hosted gateways, but send the
API key and request content without transport encryption. Use HTTP only on a
trusted network and prefer HTTPS whenever the gateway supports TLS.

## Privacy and Usage

Kimi Code is a remote provider. Prompts, conversation context, and attached
content sent through this plugin are processed by Kimi under its terms and
privacy policy. Use per-user credentials on shared deployments so each person
controls their own account, quota, and billing.

Kimi asks third-party clients to preserve their real client identity. Do not
configure a proxy to impersonate another product or alter client identification
to obtain different access.

## Troubleshooting

**Unauthorized or HTTP 401**

- Confirm the API key is from the Kimi Code Console.
- Confirm the selected model is included in your membership tier.
- K3 requires Moderato or above, and HighSpeed requires Allegretto or above.

**K3 context is smaller than expected**

- K3 context limits depend on membership.
- Moderato supports a smaller K3 context than Allegretto and higher tiers.
- Select `k3-256k` when you want the lower-quota 256K K3 route explicitly.
- Start a new chat after switching model families to avoid reusing an
  incompatible context cache.

**HTTP 429**

- Read the provider message shown in the Work activity. Kimi distinguishes
  temporary engine overload, concurrent-request limits, rolling usage limits,
  and monthly quota exhaustion.
- Retry temporary overloads after the indicated delay. Quota errors require
  waiting for the reset or changing the account plan.

**The model does not appear**

- Activate the Kimi Code plugin.
- Confirm the API key is saved for the current user or available as
  `KIMI_API_KEY`.
- Reload the model list after changing plugin settings.

## Related Docs

- [Plugin Architecture](./PLUGIN_ARCHITECTURE)
- [Working with Models](./WORKING_WITH_MODELS)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Official Kimi Code documentation](https://www.kimi.com/code/docs/en/)

---

# Work: Isolated Workspaces

URL: https://docs.librewebui.org/workspaces

# Work: Isolated Workspaces

Work is Libre WebUI's native coding-agent surface. Each Work task combines a
durable conversation, an explicit model-provider route, and a dedicated
filesystem at `/workspace`. The selected model can inspect and edit files, run
commands in a task-scoped Docker container or Kubernetes Pod, and start a
browser preview.

Work is implemented directly in Libre WebUI. It does not require Libre Claw or
another agent daemon.

:::warning Trusted users only

Every Work API requires an authenticated account with Work access. By
default that means administrators only; an administrator can open Work to
all active users from the User Management page (host-folder workspaces stay
admin-only regardless, because they bind-mount server paths). Work
deliberately lets a model execute arbitrary shell commands inside a
sandbox. Tasks use network egress unless their selected named runtime policy
disables it. Treat everyone you grant Work access as a trusted runtime
operator, not merely as a chat user.

:::

## Release Highlights

This release introduces Work as a complete task workflow:

- Separate **Work** and **Chat** actions in the main sidebar, with the active
  mode visibly selected.
- Work tasks in the normal sidebar instead of a second task rail. Existing
  task positions remain stable while runs update, and the selected task can be
  deleted directly.
- A dedicated sandbox identity and persistent Docker volume or Kubernetes PVC
  for every task. Sandboxes can be stopped or recreated without deleting task
  files.
- Durable conversation, run state, tool activity, model selection, and task
  ownership in Libre WebUI's database.
- A live, authenticated run stream for assistant text, provider-exposed
  reasoning, tool calls and results, usage, worker skills, and state changes.
- Server-owned worker skills that teach the selected model how to inspect,
  edit, verify, and preview efficiently without writing control files into the
  project.
- Tool-capable local Ollama models, Ollama Cloud models, and configured
  completion or chat provider plugins.
- A responsive Conversation/Workspace split with draggable, keyboard
  accessible sizing on desktop and a focused surface switcher on smaller
  screens.
- Integrated Files, Activity, Git, Terminal, and Preview views.
- Dark- and light-mode syntax highlighting, browser-side code formatting,
  save conflict detection, and temporary unsaved drafts.
- A dismissible, per-user disclosure when a remote model provider is selected.
- Complete Work translations across all 25 supported locales, including
  native Arabic right-to-left layout while code, paths, model identifiers, and
  command output remain left-to-right.

The persistent unit is the task workspace, not a continuously running
container. Libre WebUI starts, stops, and may recreate the task's container as
needed while retaining its named volume.

## Architecture

```mermaid
flowchart LR
    UI["Work interface"]
    API["Authenticated /api/work API"]
    DB["Libre WebUI database"]
    AGENT["Native model/tool loop"]
    PROVIDER["Selected Ollama or plugin provider"]
    CONTAINER["Task-scoped sandbox"]
    VOLUME["Task-scoped volume or PVC"]
    PREVIEW["Loopback browser preview"]

    UI --> API
    API --> DB
    API --> AGENT
    AGENT <--> PROVIDER
    AGENT --> CONTAINER
    API --> CONTAINER
    CONTAINER <--> VOLUME
    CONTAINER --> PREVIEW
```

Libre WebUI, rather than the model or browser, chooses the sandbox and
workspace names, image, mount, user, limits, network mode, and preview port. The
model receives only these tools:

- `list_files`
- `read_file`
- `write_file`
- `delete_file`
- `move_file`
- `search_files`
- `run_command`
- `start_preview`
- `stop_preview`

`delete_file` and `move_file` are path-guarded like the other file tools:
they refuse to leave the workspace, never traverse symlinks, require an
explicit recursive flag before removing a directory, and never overwrite a
move destination. Because they run through the file-helper path rather than a
shell, they also work while a preview is running, when `run_command` is
blocked.

Model requests are made by the Libre WebUI backend. They do not originate from
the Work container and do not depend on the container's network policy.

## Requirements

Work needs a configured sandbox backend:

- The default backend needs Docker installed with a reachable daemon and
  permission for the backend process to invoke `docker` (or the executable
  configured through `WORK_DOCKER_COMMAND`).
- The Kubernetes backend needs API credentials plus the namespace-scoped Role,
  RoleBinding, sandbox namespace, and NetworkPolicies created by the Helm chart
  when `work.enabled=true`.

Every backend also needs:

- A tool-capable model exposed through:
  - a healthy Ollama service, including models reached through Ollama Cloud; or
  - an active completion/chat plugin with an exact configured model and
    credentials for the current administrator.
- Enough runtime storage for the image, generated projects, and project-local
  dependencies.
- An authenticated account with Work access. Work is admin-only by default;
  an administrator can open it to all active users.

Libre WebUI checks Ollama's advertised model capabilities before creating a
run and rejects an Ollama model that does not advertise `tools`. Plugin-backed
models must support their provider's tool-calling protocol. If a selected
remote model rejects tools, the run fails; Work does not silently switch to a
different model or provider.

## Start Locally

For the simplest supported Work setup, run Libre WebUI and Docker on the same
computer as the browser:

```bash
docker info
npx libre-webui@latest
```

Open `http://localhost:8080`, sign in as an administrator, select **Work** in
the sidebar, choose a compatible model, and describe the project or change.

If Docker is missing, stopped, or inaccessible, Work shows **Runtime
unavailable** with the backend's reason and disables the Run composer. Libre
WebUI never falls back to executing Work commands directly on the host.

The runtime image is inspected on first use and pulled automatically when it
is absent. The first operation can therefore take longer than later ones.

## Using the Work Interface

### Create and revisit tasks

Select **Work** beside **Chat** in the sidebar. Enter an instruction, choose a
model, and select **Run**. The first message creates the task, its first run,
its provider route, and its persistent workspace.

Each task remains in the primary sidebar. Reopening it restores its recent
conversation, Files view, current provider/model selection, and workspace.
Older conversation messages can be loaded in pages. You can rename the task
from its title and permanently delete it from either the selected-task menu or
the sidebar.

Only one run can be active for a task. A later instruction creates another run
against the same conversation and filesystem.

### Understand task status

The interface maps durable backend states to a smaller user-facing set:

| Interface status | Backend state                | Indicator color      |
| ---------------- | ---------------------------- | -------------------- |
| Idle             | `idle`                       | `rgb(255, 255, 255)` |
| Thinking         | `preparing` or `running`     | `rgb(48, 121, 255)`  |
| Complete         | `completed`                  | `rgb(76, 212, 117)`  |
| Needs input      | `needs_input` or `cancelled` | `rgb(255, 204, 0)`   |
| Error            | `failed`                     | `rgb(255, 61, 129)`  |

Stopping an active run changes it to **Needs input** and preserves its files.
Exhausting the round or tool-call safety budget also ends in **Needs input**
after the final no-tools handoff, so incomplete work is never labeled
**Complete**.

### Resize the workspace

At the `xl` desktop breakpoint, Conversation and Workspace share a draggable
split:

- The default conversation width is 45%.
- The preferred range is 30% to 70%, subject to minimum content widths.
- The saved ratio is scoped to the signed-in user in that browser.
- Arrow keys move the separator by 2%; hold Shift for 10%.
- Home and End select the available minimum and maximum.
- Enter or double-click resets the split.

The controls follow the active writing direction. In Arabic, Conversation is
on the right, Workspace is on the left, and pointer and arrow-key resizing
continue to operate in the expected visual direction.

On smaller screens, use the Conversation/Workspace control in the task header
to switch surfaces.

### Files

The Files tab browses direct children of `/workspace`, opens strictly valid
UTF-8 text files, and saves changes back to the task volume. Invalid byte
sequences are rejected instead of being replaced with lossy placeholder
characters.

The editor provides:

- syntax highlighting in both light and dark modes for common web, systems,
  scripting, data, and markup languages;
- `Cmd/Ctrl+S` to save;
- `Shift+Alt+F` to format supported files;
- optimistic save conflict detection, so an older editor view cannot silently
  overwrite a file changed since it was opened;
- task-and-path-scoped unsaved drafts in browser session storage; and
- navigation warnings while an unsaved edit is open.

Live highlighting pauses above 8,000 characters or 400 lines to keep editing
responsive. Formatting is available up to 100,000 characters and 4,000 lines
for JavaScript/JSX, TypeScript/TSX, JSON variants, CSS/SCSS/Less, HTML,
Markdown/MDX, and YAML.

When the model changes a file you had open, the Files tab opens a red/green
Changes view showing exactly what was added and removed since the turn
started, with long unchanged runs folded away. A toolbar toggle switches
between the diff and the editor, and the `+added −removed` counters summarize
the turn at a glance. The comparison baseline is the last content your browser
saw before the turn, so files first opened after the turn show no diff.

Browser drafts are convenience state, not a backup. They are cleared after a
successful save or task deletion and normally disappear when the browser
session ends.

### Activity

The Activity tab shows tool calls, tool results, file operations, command
output, and errors. Tool metadata can be expanded in the conversation. Command
and tool output is displayed left-to-right even when the surrounding
interface is right-to-left.

While a run is active, Libre WebUI opens an authenticated server-sent event
stream and renders progress as the backend receives it. The stream can carry:

- an initial `snapshot` and later `run_state` changes;
- `reasoning_delta` when the selected provider explicitly exposes reasoning;
- `assistant_delta` text;
- `tool_call` and `tool_result` activity;
- `usage` measurements;
- `skill_loaded` notifications for server-supplied worker guidance; and
- terminal `error` or `done` events.

Reasoning availability and granularity depend on the model and provider.
Libre WebUI displays only reasoning content the provider returns through its
API; it cannot recover hidden chain-of-thought, and some models provide no
reasoning stream at all. Assistant text and tool activity still stream when
supported independently of reasoning.

Output is deliberately bounded. A truncated result is not proof that a command
produced no additional output; ask the model to inspect a narrower result or
run a more focused command.

### Git

The Git tab provides local source-control operations for the task's own
`/workspace`:

- initialize a repository with a `main` branch;
- inspect porcelain status, ahead/behind counts, and up to 20 recent commits;
- inspect a bounded textual diff for a changed path;
- stage up to 200 explicitly selected paths at a time;
- commit staged changes using the signed-in administrator's username and
  email, or an instance-local no-reply address when the account has no email;
- create a local branch after the first commit; and
- switch to an existing local branch when the worktree is clean.

This surface is intentionally **local-only**. It has no clone, fetch, pull,
push, remote-management, arbitrary Git-command, token, SSH-key, or pull-request
control. Those operations need a separate trusted credential broker, ideally a
GitHub App or equivalent installation token scoped to one repository and one
operation. Do not put long-lived Git credentials in `/workspace`, the task
container environment, or repository configuration.

Git reads can run while the task is otherwise idle or active. Git writes are
rejected while a model run, interactive terminal, or preview owns the task
container. Branch switching additionally requires a clean worktree. This
prevents the UI from racing the model or a long-lived process over the same
files.

Every UI Git command is a fixed argument array executed as UID/GID `1000:1000`
inside the task container; user input is never evaluated by a shell. The
runtime disables system/global Git configuration, prompts, hooks, credential
helpers, commit signing, submodule recursion, external diff drivers, textconv,
and network protocols for this surface. It refuses repositories whose
worktree is not exactly `/workspace` or whose Git/common directory resolves
outside `/workspace`. Git write actions that could process file content are
also blocked when repository configuration defines an executable clean,
smudge, or process filter.

These controls protect the Libre WebUI Git API. An administrator can still use
the Terminal, and the model can still use `run_command`, to run ordinary Git
commands inside the sandbox. The sandbox and deployment boundary therefore
remain the security controls for arbitrary commands.

### Built-in worker skills

Every run receives a server-owned workspace guide. It explains the durable
`/workspace` boundary, read-only container root, temporary process and `/tmp`
state, network policy, command and output limits, and preview lifecycle. Its
built-in skills direct the model to:

- inspect project instructions, manifests, lockfiles, scripts, and current
  repository state before editing;
- preserve unrelated work and batch independent reads or searches;
- continue through implementation instead of stopping after a plan;
- run focused verification before broader checks;
- diagnose a failure instead of blindly retrying it; and
- verify the application before starting the preview as the final long-lived
  process.

The guide exists in model context only. Libre WebUI does not create an
`AGENTS.md`, skill directory, or other control file in the user's workspace.
Project-provided instructions remain project guidance and cannot override the
container or tool security boundary.

### Terminal

The Terminal tab attaches an interactive shell to the same sandboxed container
the model works in, so an administrator can inspect state, run a build by
hand, or debug what a run left behind without leaving the browser.

The shell runs under the identical container policy as every model tool: the
unprivileged `1000:1000` user, working directory `/workspace`, inside the
already-hardened, capability-dropped container. A terminal grants no privilege
the model's `run_command` tool does not already have — it is a human-facing
interface to the same boundary, not a way around it.

Operational behavior:

- **Authentication** — the browser exchanges its normal Authorization header
  over HTTP for a short-lived, one-use ticket bound to the Work-terminal
  protocol and exact task. Only that ticket and the task ID appear on the
  `/ws/work-terminal` upgrade URL. Before every shell input, Libre re-checks
  the current account status, Work access, task existence, and task ownership.
  Revocation closes the shell and releases its runtime lease immediately.
- **Origin checks** — when `CORS_ORIGIN` or `BASE_URL` is configured, browser
  upgrades must match one of those origins. Configure at least one for remote
  deployments. Originless upgrades remain available for Electron and
  non-browser clients, but still require the same task-bound ticket and live
  authorization checks; use TLS, firewall, and reverse-proxy policy to control
  those clients.
- **Admission** — an open terminal takes a runtime lease exactly like a
  command or preview, and counts against `WORK_MAX_ACTIVE_RUNTIMES_*`.
- **Container lifetime** — an attached terminal keeps the container running
  and prevents the idle-stop path from removing it mid-session.
- **Concurrency** — `WORK_TERMINAL_MAX_SESSIONS_PER_TASK` (default 2) bounds
  simultaneous shells per task.
- **Idle timeout** — `WORK_TERMINAL_IDLE_TIMEOUT_MS` (default 15 minutes)
  closes an untouched session and releases its lease.
- **While a run is active** — the tab explains that the model owns the
  container and opens the shell once the turn finishes.

The terminal talks to the Docker Engine **API directly**, because a TTY
session requires a hijacked bidirectional stream that the Docker CLI only
provides to a real controlling terminal. It uses `WORK_DOCKER_SOCKET`,
otherwise `DOCKER_HOST` — a `unix://` socket or a plain-HTTP `tcp://`
endpoint such as a socket proxy, whose HTTP-aware forwarding carries the
hijacked stream over a standard `Connection: Upgrade` tunnel — otherwise
`/var/run/docker.sock`. A `DOCKER_HOST` this client cannot speak to
(`ssh://`, or `tcp://` with `DOCKER_TLS_VERIFY` set) reports the terminal as
unavailable with that reason rather than silently attaching elsewhere; the
rest of Work keeps working. On the Kubernetes backend the same session
rides the exec subresource as a TTY WebSocket through the API server —
resize frames included — with no Docker endpoint involved.

Terminal sessions are interactive, not recorded. Commands typed there do not
appear in the task's Activity timeline.

### Preview

The Preview tab starts, stops, embeds, and opens the generated web application.
When the command field is empty, Libre WebUI inspects the workspace and:

- runs a root `package.json` `dev` script with the required host and port;
- serves a root `index.html` with a bundled, zero-dependency static server; or
- uses the same rules for a single app in a nested directory.

Root applications take precedence. If multiple equally likely nested apps are
found, or no supported entry point exists, Work returns an actionable error
instead of attempting an unrelated npm command. Enter a custom command before
selecting **Start preview** for other project layouts or servers. Custom
commands start in `/workspace`, so include the relative directory when needed,
for example `cd apps/web && npm run dev -- --host 0.0.0.0 --port 4173`. A
custom process must listen on `0.0.0.0` and the configured
`WORK_PREVIEW_PORT`. Work waits up to 15 seconds for the port to become ready.

The model can also start the preview through its `start_preview` tool. This is
the only supported way for a model to leave a process running. Ordinary
`run_command` calls clean up background descendants when the command finishes.

## Providers, Routing, and Data Disclosure

### Supported provider routes

| Route                    | Validation and behavior                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| Local Ollama             | Ollama must be healthy and the exact model must advertise tool support.                           |
| Ollama Cloud             | Routed explicitly through Ollama; cloud-suffixed models display the remote-provider disclosure.   |
| Completion/chat plugin   | Plugin must be active, list the exact model, and have a credential for the current administrator. |
| Anthropic plugin         | Uses Work's Anthropic messages and tool-use adapter.                                              |
| Gemini plugin            | Uses Work's Gemini contents and function-calling adapter.                                         |
| Other compatible plugins | Use the OpenAI-style messages, tools, and tool-choice request shape.                              |

Provider type and plugin ID are stored on both the task and each run. A model
name never chooses the route by itself. Activating a plugin with the same model
name as an Ollama model cannot intercept an existing task.

### What a provider receives

For each model round, the selected provider can receive:

- the Work system prompt;
- the built-in worker skills and current runtime limits;
- up to the most recent 30 user/assistant conversation messages, bounded to
  256 KB;
- Work tool definitions;
- assistant tool-call history; and
- tool results, which can include directory listings, requested file contents,
  search results, command output, and errors.

The named volume is not uploaded wholesale. However, any file content or
command output returned through a tool becomes part of the model conversation
and is sent to the selected provider. Review remote providers' retention,
training, pricing, and usage policies before using sensitive source code.

Provider credentials remain on the Libre WebUI backend, whether they are
configured deployment-wide or for an individual user. They are used for
backend model requests and are never mounted into the Work container.

Application-layer credential encryption is not whole-task encryption. Work
conversations, tool results, command output, and task metadata are ordinary
database content, while workspace files and dependencies are ordinary files in
the task's Docker volume or Kubernetes PVC. Use host access controls and disk
encryption when the deployment's threat model requires encryption at rest.

### Remote-provider disclosure

Work treats plugin models and Ollama names ending in `:cloud` or `-cloud` as
remote for disclosure purposes. Selecting one opens a dismissible notice that
explains provider data flow and the possibility of multiple billable calls.
The dismissal preference is remembered per Libre WebUI user.

All provider routes use the same `WORK_MAX_AGENT_ROUNDS` budget, 48 rounds by
default. There is no separate 12-round plugin clamp. The tool-call safety
budget is the larger of 128 calls or eight calls per configured round. When the
round budget is exhausted, Libre WebUI asks the model for one final no-tools
handoff describing completed work, checks, blockers, and remaining steps. It
then records the terminal run as **Needs input** instead of exposing a raw
round-limit exception or marking incomplete work complete. A follow-up run
continues in the same durable workspace. A single Work run can still make many
billable provider requests.

## Host Folder Workspaces (Opt-In)

On the Docker backend, a task's `/workspace` is normally a named volume that
exists only for that task, so the model cannot reach your real files. A Docker
deployment can instead allow a task to be bound to an actual folder on the
host. Kubernetes rejects host-folder workspaces and uses a task-owned PVC.

Set both variables, then restart the backend:

```bash
WORK_HOST_WORKSPACES_ENABLED=true
WORK_HOST_WORKSPACE_ROOTS=/Users/you/Projects
```

`WORK_HOST_WORKSPACE_ROOTS` is a `:`-separated list of roots; it defaults to the
server user's home directory. When the feature is on, the Work landing screen
gains an optional **Workspace folder** field. Leave it blank and the task
behaves exactly as before, with its own isolated volume.

Before a path is accepted it must be absolute, exist, be a directory, and
resolve — through any symlinks — to a location inside one of the configured
roots. Directories named `.ssh`, `.gnupg`, `.aws`, `.config`, `.kube`,
`.docker`, `.claude`, `.libre-webui`, or `node_modules` are refused outright.
The resolved path is stored with the task and shown in the task header, so it is
always visible which folder a task is operating on.

:::caution This narrows the sandbox

A host workspace means the model reads and writes your real files, and the
container's other protections — non-root user, dropped capabilities, resource
limits — no longer stand between it and that directory. Keep the feature
disabled unless you want it, keep the roots as narrow as possible, and prefer
directories that are under version control.

:::

## Persistence and Runtime Lifecycle

Libre WebUI separates durable state from execution state:

| State                                       | Storage                                | Lifetime                                                                 |
| ------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------ |
| Task ownership, title, provider, and status | Libre WebUI database                   | Until the task or owning user is deleted                                 |
| Runs, errors, messages, and tool activity   | Libre WebUI database                   | Until the task is deleted                                                |
| Workspace files                             | Task-specific Docker volume or K8s PVC | Survive run cancellation, preview stop, sandbox restart, and app restart |
| Root filesystem and temporary files         | Task-specific container or Pod         | Disposable; may be stopped or recreated                                  |
| Preview process                             | Running task sandbox                   | Ephemeral; retained only while verified healthy                          |
| Unsaved editor draft                        | Browser session storage                | Temporary browser-session convenience state                              |

Every task gets a server-generated UUID. Its sandbox and workspace names are
derived on the backend and are never accepted from a browser request. Libre
WebUI creates the runtime resources with managed and task-ownership labels.
Before
reuse or deletion, it verifies the task-ownership label and refuses a resource
whose label belongs to another task.

Sandboxes are prepared on demand. File-helper operations stop an otherwise
idle sandbox, commands stop the sandbox after completion, and a verified
preview may keep it running so the user can inspect the app. The durable
workspace is mounted again when the same task sandbox is restarted or
recreated.

Administrators can define **named runtime policies** from the User
Management page: presets combining a runtime image, memory/CPU/PID limits,
a workspace size (Kubernetes), an idle timeout, and a network default. A
task created under a policy runs with that configuration; every field a
policy leaves empty inherits the deployment's global values, and deleting
a policy returns its tasks to those globals on their next container
recreation. Policies adjust resources only — the hardening profile
(non-root, read-only rootfs, dropped capabilities, network isolation) is
not a policy field and cannot be weakened per policy.

`WORK_RUNTIME_IDLE_TIMEOUT_MS` bounds how long that preview grace lasts:
when set, a sweep stops any sandbox that has seen no activity — no command
finished, no terminal attached, no preview request through the signed
proxy — for that many milliseconds, freeing its admission slot. Stopping is
cheap and the workspace persists, so an idled preview simply restarts on
the next use. The default (`0`) keeps today's behavior: a preview runs
until it is stopped explicitly.

On backend startup, active runs are marked failed and preview state is cleared
— the agent loop and preview proxy died with the process and cannot be resumed.
The selected driver then lists its managed containers or Pods in one labeled
query. Running sandboxes owned by known tasks are stopped because an
interrupted command may still be executing without a supervisor; sandboxes
already at rest remain unchanged; and managed sandboxes whose task row no
longer exists are removed. Ownership comes from the task label, never the
resource name. Orphan removal assumes one Libre WebUI instance owns a runtime
namespace or Docker daemon. Do not point two instances at the same Work
resources. If the driver cannot prove cleanup, Work stays fail-closed, retries
every 10 seconds, and blocks new mutable operations until runtime access is
restored.

## Network Behavior

:::caution Verify the selected network policy

Tasks without a named runtime policy start network-enabled. An administrator
can define a named policy whose network default is off, and the creator can
select that policy when creating a task. There is no independent per-task
network switch, and changing the policy later requires recreating the sandbox
before the new runtime configuration takes effect.

:::

On the Docker backend, networked tasks attach to a dedicated managed bridge
network
(`libre-webui-work` by default, `WORK_NETWORK_NAME`) created with
inter-container communication disabled
(`com.docker.network.bridge.enable_icc=false`). Two consequences follow:

- one Work sandbox cannot open connections to another Work sandbox; and
- a Work sandbox cannot reach the deployment's own containers on Docker's
  shared default bridge, including a co-located database or Ollama container
  that is not deliberately published.

Libre WebUI refuses to start a networked task if a network with the configured
name already exists but is not the managed one, rather than silently attaching
sandboxes to an operator's network.

On Kubernetes, the sandbox Pod carries the same network-enabled label. The
Helm chart installs a default-deny NetworkPolicy, preview-only ingress, and
internet egress only for network-enabled Pods, excluding the configured
`work.networkPolicy.blockedEgressCidrs`. NetworkPolicy is effective only when
the cluster CNI enforces it; see the [Kubernetes guide](./KUBERNETES).

Egress to the outside world is still permitted, because package downloads,
remote Git operations, and external APIs are what makes Work useful. This is
not an outbound firewall. Generated code may still be able to reach:

- services on the Docker host;
- systems on the host's local network;
- internet services; and
- infrastructure metadata endpoints, depending on the deployment.

### Egress policy hooks

For a stricter boundary, use these in combination:

- **`WORK_RUNTIME_DNS` (Docker)** — comma-separated IPv4/IPv6 resolver
  addresses forced onto every networked sandbox (`--dns`). Pointing this at a filtering
  resolver gives you name-based allow/deny lists without patching Libre WebUI.
  Non-address entries are rejected and logged, so the value can never inject
  additional Docker flags.
- **Host or upstream firewall rules (Docker)** on the managed bridge's subnet,
  which is stable because the network is named and managed.
- **`WORK_NETWORK_NAME` (Docker)** pointed at a network you pre-create with
  your own driver options — Libre WebUI verifies it carries the managed label
  and ICC-disabled option, so create it with both.

DNS filtering constrains name resolution, not raw IP egress. A deployment that
must guarantee no direct-IP egress needs host-, cluster-, or upstream-level
firewall rules as well.

Do not assume that placing code in Work prevents it from transmitting data.
Grant Work access only to trusted users. Use a named network-disabled runtime
policy when a task should start offline; there is no deployment-wide
environment variable that changes the default policy.

Network access does not add credentials. Libre WebUI does not mount SSH keys,
cloud credentials, browser profiles, the host home directory, or the Docker
socket into task containers. Code can still transmit any credentials or
secrets that a user or model writes into `/workspace`.

This sandbox traffic is separate from model traffic. Ollama and plugin
requests are always sent by the Libre WebUI backend to the explicitly selected
provider route.

## Sandbox Security Boundary

A Docker Work container:

- runs as non-root UID/GID `1000:1000`;
- uses `/workspace` as its working directory;
- mounts only the selected task's named volume at `/workspace`;
- uses a read-only root filesystem and a bounded `/tmp` temporary filesystem;
- drops all Linux capabilities;
- enables `no-new-privileges`;
- is non-privileged and uses an init process;
- applies CPU, memory, process, command-time, and output limits;
- pins swap to the memory limit (`--memory-swap` equals `--memory`), so the
  memory cap cannot be sidestepped by swapping;
- attaches to the managed sandbox network with inter-container communication
  disabled, or to no network at all; and
- publishes only the configured preview port to a Docker-assigned loopback
  host port.

Every one of these is re-verified against `docker inspect` before a container
is reused, and the whole set is hashed into the `ai.libre-webui.policy`
container label. A container whose policy predates a Libre WebUI upgrade is
destroyed and recreated rather than reused, so a hardening change reaches
existing tasks automatically.

The Kubernetes driver applies the equivalent Pod security context: non-root
UID/GID, read-only root filesystem, `RuntimeDefault` seccomp, no privilege
escalation, all capabilities dropped, bounded ephemeral storage, resource
limits, no ServiceAccount token, and a task-owned PVC at `/workspace`. It
verifies task labels and the policy fingerprint before reusing or deleting a
Pod or PVC.

Path validation rejects absolute paths, traversal segments, backslashes, NUL
characters, and overlong paths. File helpers resolve real paths and reject
symlink escapes. Writes use a temporary file and atomic rename.

These controls reduce accidental host exposure; they do not make Work a
virtual machine or a safe malware-analysis environment. Containers share the
runtime host's kernel. A Docker, Kubernetes, runtime, image, dependency, or kernel
vulnerability can cross the intended boundary.

Docker named volumes do not have an independent disk quota. A generated
project or package installation can exhaust Docker storage, so monitor volume
growth and apply host-level storage limits. Kubernetes requests a PVC size;
actual quota enforcement depends on the selected storage provisioner.

## Docker Production Hardening Checklist

This checklist is specific to the Docker backend. Kubernetes operators should
also validate the chart's namespace-scoped RBAC, Pod security context, storage
class, and CNI NetworkPolicy enforcement as described in the
[Kubernetes guide](./KUBERNETES).

The application can set container flags, validate workspace paths, and guard
its own API. It cannot enforce host firewall policy, storage-driver quotas, or
the privilege level of the Docker daemon it is given. Treat these as explicit
deployment work for a private client instance.

### 1. Isolate Docker control

The main Libre WebUI container needs daemon control to create and inspect Work
containers. A mounted Docker socket is therefore a control-plane credential,
not an ordinary data mount: compromising the web application can become a
Docker-host compromise.

The first mitigation ships in this repository:
`docker-compose.socket-proxy.yml` keeps the socket out of the Libre WebUI
container entirely. A socket proxy holds `/var/run/docker.sock` on an
internal network and forwards only the API sections Work uses — containers,
images, volumes, networks, exec, info — while swarm, secrets, configs,
build, commit, and system endpoints are denied before they reach the daemon.
Libre WebUI is pointed at it with `DOCKER_HOST=tcp://docker-socket-proxy:2375`
and needs no socket mount and no socket-group membership; the CLI, the
interactive terminal, and Docker diagnostics all follow that endpoint. The
proxy narrows the API surface, not the blast radius of the endpoints it does
forward: whoever can create containers can still bind-mount host paths, so
the boundary below still matters.

For a stronger production boundary, run Libre WebUI and its Work daemon on a
dedicated VM with no unrelated workloads. Stronger still, give Work a
dedicated rootless Docker daemon or a separate runtime host and expose only
that daemon to Libre WebUI. Verify file ownership, preview routing, cleanup,
and terminal support against that daemon before rollout. Merely mounting the
same rootful host socket read-only does not make the Docker API read-only.

### 2. Block sandbox-to-host management access

Disabling inter-container communication prevents Work sandboxes from reaching
one another; it does not prevent them from reaching services bound on the
Docker host. Inspect the actual managed bridge and subnet rather than assuming
an address:

```bash
docker network inspect libre-webui-work \
  --format 'id={{.Id}} subnets={{range .IPAM.Config}}{{.Subnet}} {{end}}'
ss -lntup
```

Use the host's persistent firewall manager to reject traffic arriving from
that bridge to host management services, especially SSH, the Docker API,
databases, and monitoring/admin ports. Test the rule from a disposable
container attached to `libre-webui-work`, test allowed package downloads, then
make the rule persistent. Docker's `DOCKER-USER` chain controls forwarded
traffic; traffic whose destination is the Docker host itself may need an
`INPUT`/input-hook rule on the bridge interface as well.

### 3. Constrain outbound destinations

Block cloud metadata endpoints, private infrastructure ranges, and client LAN
ranges from the Work subnet unless a project explicitly needs them. Combine a
filtering resolver through `WORK_RUNTIME_DNS` with host or upstream firewall
rules. DNS filtering alone is bypassable with a literal IP address. An HTTP
proxy alone is also insufficient while arbitrary commands can open direct
network connections; enforce the routing policy outside the container.

Maintain separate named runtime policies when clients need different behavior, for example
an offline/no-network runtime, a package-registry-only runtime, and an open
egress runtime. The named policy controls whether Libre attaches the sandbox
network; external firewall and proxy rules still enforce destination-level
restrictions for a network-enabled policy.

### 4. Enforce real storage quotas

CPU, memory, swap, and PID limits do not limit the named volume. Before serving
multiple clients, choose a storage backend with enforceable per-workspace
quotas: for example XFS project quotas, quota-backed logical volumes, or a
volume/PVC driver with a size limit. The default Docker `local` driver on an
ordinary ext4 filesystem does not gain a reliable per-volume quota merely by
documenting a size value.

Monitor both each `ai.libre-webui.managed=true` volume and the Docker data root,
alert before the filesystem is full, and test the failure mode. A UI counter or
periodic `du` check can warn, but it is not an enforcement boundary because a
container can consume the remaining disk between checks.

### 5. Verify the deployed policy

After every image or daemon-policy change, create a disposable Work task and
verify the effective state with `docker inspect`: non-root UID, read-only root,
all capabilities dropped, `no-new-privileges`, memory/swap/CPU/PID limits,
only the task volume mounted, and the expected network. Also verify that the
main Libre WebUI container has only the intended mounts and that public ingress
reaches the app through the authenticated reverse proxy or tunnel—not through
an accidentally published Docker or preview port.

## Preview Security and Reachability

For a Docker task, the driver publishes the configured preview port to a
dynamically assigned port on backend loopback. For Kubernetes, the in-cluster
backend targets the sandbox Pod IP directly. The model and browser cannot
choose an arbitrary upstream. Libre WebUI signs a capability URL for the exact
task and endpoint, verifies that the preview is still running on every request,
and proxies HTTP and WebSocket traffic through `/api/work/previews`. Stopping
or restarting the preview revokes the old URL.

Preview responses strip Libre WebUI credentials and upstream cookies. HTML is
constrained by both an iframe sandbox and response CSP that allow scripts,
forms, modals, and downloads without granting same-origin access. The CSP also
protects a preview opened in a separate tab. Generated application code
remains untrusted and can use network egress to transmit anything it can read
from its own workspace or browser inputs. Treat a running preview URL as a
short-lived secret and do not share it.

Because the browser loads the proxy on Libre WebUI's own public origin, remote
browsers and HTTPS reverse proxies work without exposing Docker ports or Pod
IPs and without triggering mixed-content blocking. Reverse proxies must
preserve WebSocket upgrades for `/api/work/previews/`; the provided Nginx
configuration does so.

The main application permits only its own origin and Cloudflare Turnstile as
frame sources. Preview responses bypass the main Helmet policy so they can
stream request bodies and apply the narrower sandbox policy described above.
Cross-origin embedder policy remains disabled because generated dev servers do
not normally emit compatible resource headers.

## Deployment Matrix

Work availability follows the machine and process running the Libre WebUI
backend, not merely the browser or desktop interface.

| Deployment                                | Work runs and files                                                                                                                                                                                                                                                            | Embedded preview                                                                                  |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `npx libre-webui` on a local computer     | Supported when Docker is installed, running, and callable by the backend user.                                                                                                                                                                                                 | Supported through the signed application-origin proxy.                                            |
| Source development on a local computer    | Supported under the same Docker and provider requirements.                                                                                                                                                                                                                     | Supported through the development API origin on port 3001.                                        |
| Electron desktop client                   | Conditional. Electron uses an external Libre WebUI backend and does not provide a separate Work runtime.                                                                                                                                                                       | Supported through that backend's signed proxy URL.                                                |
| Bare-metal or VM backend on a remote host | Runs, files, and provider calls work when Docker is available on that host.                                                                                                                                                                                                    | Supported when the public reverse proxy preserves HTTP and WebSocket traffic.                     |
| Standard repository Docker Compose        | Supported by default: the image ships the Docker CLI and the Compose file mounts the host Docker socket.                                                                                                                                                                       | Supported through the same public Libre WebUI origin.                                             |
| Current Kubernetes/Helm deployment        | Supported with `--set work.enabled=true`: sandboxes run as Pods with PVC workspaces (runs, files, commands, git, interactive terminals), under a namespace-scoped Role and default-deny NetworkPolicies — no Docker socket anywhere. See the [Kubernetes guide](./KUBERNETES). | Supported when the backend runs in-cluster: the signed proxy targets the sandbox Pod IP directly. |

### Running Work when Libre WebUI is itself in Docker

Every repository Compose file enables Work: the image ships the Docker CLI and
the Compose file mounts `/var/run/docker.sock`. `docker compose up -d` is all
that is required.

Work drives the host daemon through that socket, so task containers are
**siblings** of the Libre WebUI container rather than children. They appear in
`docker ps` on the host and are cleaned up by the same lifecycle rules as a
native install.

Mounting the Docker socket into a web application gives that container
root-equivalent control over the Docker host. Work cannot function without it,
so Libre WebUI enables it rather than shipping a feature that silently does
nothing. The consequence is explicit: **every Libre WebUI administrator is
effectively an administrator of the Docker host.** Operators own the
daemon-security, network, lifecycle, backup, and access-control consequences.
Delete the `/var/run/docker.sock` line from your Compose file to turn Work off;
nothing else depends on it.

To keep Work without handing the socket to the web application, deploy with
`docker-compose.socket-proxy.yml` instead: a socket proxy on an internal
network holds the socket and forwards only the API sections Work uses, and
Libre WebUI reaches it through `DOCKER_HOST`. See
[Isolate Docker control](#1-isolate-docker-control) for what that boundary
does and does not cover.

Three conditions must hold, and the Work panel names whichever one fails:

1. **The Docker CLI must exist in the image.** It ships in the official image; a
   custom image needs `docker-cli`, or `WORK_DOCKER_COMMAND` pointing at one.
   Otherwise: `The "docker" CLI is not installed…`.
2. **The socket must be mounted.** Otherwise: `No Docker daemon is reachable…`.
3. **The backend user must be in the socket's group.** The image runs as
   `nodejs` (uid 1001) and the socket is typically owned by `root` or `docker`,
   so Compose passes `group_add: ['${DOCKER_GID:-0}']`. The default suits Docker
   Desktop; a Linux host needs its own group id. Otherwise: `The Docker socket
is mounted but the Libre WebUI user cannot open it…`.

```bash
# Read the socket's group as seen INSIDE a container. A macOS host reports a
# different value, because Docker Desktop proxies the socket through a VM.
echo "DOCKER_GID=$(docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  alpine stat -c '%g' /var/run/docker.sock)" >> .env
docker compose up -d --force-recreate
```

Task preview ports remain bound to Docker host loopback. Libre WebUI exposes
each running preview through a signed same-origin proxy URL, including HTTP
assets and WebSocket upgrades. This works behind HTTPS and remote tunnels
without opening the ephemeral Docker ports to the network. Preview documents
receive a restrictive browser sandbox policy, and stopping or restarting a
preview revokes its previous URL.

Concurrency is capped separately: `WORK_MAX_ACTIVE_RUNTIMES_PER_USER` defaults
to `2` and `WORK_MAX_ACTIVE_RUNTIMES_GLOBAL` to `3`, so an administrator can
run a second task while the first is busy. The capabilities response reports
both limits and the live occupancy. Raise them if the host has memory and CPU
to spare.

For Kubernetes, install the chart with `work.enabled=true` instead of exposing
a node runtime socket. The chart creates the scoped RBAC, sandbox namespace,
network policies, and Pod/PVC configuration described in the
[Kubernetes guide](./KUBERNETES).

## Runtime Configuration

Work reads these variables in the backend process:

| Variable                              | Default                                                                                       | Purpose                                                    |
| ------------------------------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `WORK_RUNTIME_BACKEND`                | `docker`                                                                                      | Sandbox driver: `docker` or `kubernetes`                   |
| `WORK_RUNTIME_IMAGE`                  | `node:22.22-bookworm@sha256:2d178f2785b96dfbf62a416ca2e40f50e30150b4ff3320d706f0d96e90600eb3` | Image used for task sandboxes                              |
| `WORK_DOCKER_COMMAND`                 | `docker`                                                                                      | Docker-backend CLI executable                              |
| `WORK_COMMAND_TIMEOUT_MS`             | `120000`                                                                                      | Default command timeout                                    |
| `WORK_MAX_OUTPUT_CHARS`               | `50000`                                                                                       | Maximum captured command/search output                     |
| `WORK_MAX_AGENT_ROUNDS`               | `48`                                                                                          | Provider-agnostic model/tool round budget per run          |
| `WORK_MEMORY_LIMIT`                   | `2g`                                                                                          | Per-container memory limit                                 |
| `WORK_CPU_LIMIT`                      | `2`                                                                                           | Per-container CPU limit                                    |
| `WORK_PIDS_LIMIT`                     | `256`                                                                                         | Per-container process limit                                |
| `WORK_PREVIEW_PORT`                   | `4173`                                                                                        | Port the app must listen on inside the container           |
| `WORK_PREVIEW_BIND`                   | `127.0.0.1`                                                                                   | Host interface the preview port is published on            |
| `WORK_MAX_ACTIVE_RUNTIMES_GLOBAL`     | `3`                                                                                           | Concurrent container-backed tasks per Libre WebUI instance |
| `WORK_MAX_ACTIVE_RUNTIMES_PER_USER`   | `2`                                                                                           | Concurrent container-backed tasks per administrator        |
| `WORK_MAX_TASKS_GLOBAL`               | `500`                                                                                         | Persisted Work task limit per Libre WebUI instance         |
| `WORK_MAX_TASKS_PER_USER`             | `100`                                                                                         | Persisted Work task limit per administrator                |
| `WORK_NETWORK_NAME`                   | `libre-webui-work`                                                                            | Managed sandbox bridge network for networked tasks         |
| `WORK_RUNTIME_DNS`                    | unset                                                                                         | Comma-separated resolver IPs forced onto networked tasks   |
| `WORK_DOCKER_SOCKET`                  | `DOCKER_HOST` if `unix://` or `tcp://`, else `/var/run/docker.sock`                           | Docker Engine endpoint used for interactive terminals      |
| `WORK_TERMINAL_MAX_SESSIONS_PER_TASK` | `2`                                                                                           | Simultaneous interactive terminals per task                |
| `WORK_TERMINAL_IDLE_TIMEOUT_MS`       | `900000`                                                                                      | Idle timeout before a terminal session closes              |
| `WORK_RUNTIME_IDLE_TIMEOUT_MS`        | `0` (disabled)                                                                                | Stop a sandbox after this much inactivity (previews too)   |
| `WORK_K8S_NAMESPACE`                  | `libre-webui-work`                                                                            | Kubernetes sandbox Pod/PVC namespace                       |
| `WORK_K8S_STORAGE_CLASS`              | cluster default                                                                               | StorageClass for Kubernetes workspace PVCs                 |
| `WORK_K8S_WORKSPACE_SIZE`             | `5Gi`                                                                                         | Default per-task Kubernetes PVC size                       |
| `WORK_K8S_POD_READY_TIMEOUT_MS`       | `900000`                                                                                      | Maximum wait for a sandbox Pod to become ready             |
| `WORK_K8S_POD_GONE_TIMEOUT_MS`        | `60000`                                                                                       | Maximum wait for a deleted sandbox Pod to disappear        |

Use a fixed image version or digest in production. A mutable image tag can
change both the available command-line tools and the security boundary without
changing Libre WebUI.

Run, preview, file-helper, command, and sandbox-recreation operations share
the same in-process capacity accounting. A nested operation on an already
counted task does not count as another task. Requests over a task or runtime
admission limit return HTTP 429.

### Fixed protocol and UI limits

| Item                                   | Limit                                                       |
| -------------------------------------- | ----------------------------------------------------------- |
| New task or run message                | 65,536 characters and UTF-8 bytes                           |
| Model identifier on task create/update | 500 characters and UTF-8 bytes                              |
| Plugin provider ID                     | 200 characters                                              |
| Active runs per task                   | 1                                                           |
| Command text                           | 20,000 characters                                           |
| Command timeout requested by a tool    | 1 to 600 seconds                                            |
| Preview readiness                      | 15 seconds                                                  |
| File read/write                        | 2,000,000 bytes of UTF-8 text                               |
| Direct directory listing               | First 1,000 entries                                         |
| Message page                           | Up to 200 messages and 1,000,000 bytes                      |
| Persisted individual message           | 100 KB                                                      |
| Conversation context sent to a model   | Last 30 user/assistant messages, up to 256 KB               |
| Persisted tool output                  | About 20,000 source characters plus a marker                |
| Live editor highlighting               | 8,000 characters and 400 lines                              |
| Browser-side formatting                | 100,000 characters and 4,000 lines                          |
| Git status output                      | 2,000,000 captured characters                               |
| Git diff output                        | 600,000 captured characters                                 |
| Git history                            | 20 local commits                                            |
| Paths in one Git stage request         | 200                                                         |
| Git commit message                     | 4,000 characters                                            |
| Agent loop, every provider route       | 48 rounds by default, configured by `WORK_MAX_AGENT_ROUNDS` |
| Tool-call safety budget                | `max(128, configured rounds × 8)` calls                     |

File access is for UTF-8 text. The integrated editor is not a binary-file
editor, and a file larger than 2 MB cannot be opened through the Work file API.

## API Summary

All endpoints are under `/api/work` and require authentication plus current
Work access from the database. Work is admin-only by default; an administrator
can open ordinary task operations to active users. Host-folder selection and
administrative policy/access endpoints remain admin-only.

| Method   | Path                                | Purpose                                           |
| -------- | ----------------------------------- | ------------------------------------------------- |
| `GET`    | `/capabilities`                     | Selected runtime/provider availability and limits |
| `GET`    | `/tasks`                            | List the current administrator's tasks            |
| `POST`   | `/tasks`                            | Create a task and its first asynchronous run      |
| `GET`    | `/tasks/:id`                        | Load task state and recent messages               |
| `GET`    | `/tasks/:id/messages`               | Page older messages                               |
| `PATCH`  | `/tasks/:id`                        | Rename or change the explicit model route         |
| `DELETE` | `/tasks/:id`                        | Remove the task and durable workspace             |
| `POST`   | `/tasks/:id/runs`                   | Start a follow-up run                             |
| `GET`    | `/tasks/:taskId/runs/:runId/events` | Stream authenticated live run events using SSE    |
| `POST`   | `/tasks/:id/cancel`                 | Cancel the active run                             |
| `GET`    | `/tasks/:id/files`                  | List a workspace directory                        |
| `GET`    | `/tasks/:id/file`                   | Read a workspace text file                        |
| `PUT`    | `/tasks/:id/file`                   | Save a workspace text file                        |
| `GET`    | `/tasks/:id/git`                    | Read guarded local Git status and history         |
| `GET`    | `/tasks/:id/git/diff`               | Read a bounded local diff                         |
| `POST`   | `/tasks/:id/git/init`               | Initialize local Git                              |
| `POST`   | `/tasks/:id/git/stage`              | Stage explicit workspace paths                    |
| `POST`   | `/tasks/:id/git/commit`             | Commit staged changes                             |
| `POST`   | `/tasks/:id/git/branches`           | Create a local branch                             |
| `POST`   | `/tasks/:id/git/switch`             | Switch to an existing clean local branch          |
| `POST`   | `/tasks/:id/preview/start`          | Start the managed preview                         |
| `POST`   | `/tasks/:id/preview/stop`           | Stop the managed preview                          |

The task ID is always checked against the authenticated owner. Current account
status, role, and Work-access policy are read from the database on each request,
so revocation takes effect even if an older JWT contains stale role claims.

The task update schema retains a backend `networkEnabled` field for internal
compatibility. It is not exposed as an independent Work UI control. Select a
named runtime policy with the intended network default when creating the task;
do not use the raw field as a durable configuration API.

## Deletion, Account Changes, and Backup

### Task deletion

Task deletion is intentionally destructive:

1. The backend marks the task as retiring so no new mutable operation can
   begin.
2. An active run is cancelled and the task sandbox is stopped.
3. Libre WebUI validates the task-ownership labels on the runtime resources.
4. The container/Pod and named volume/PVC are removed.
5. The database task is deleted, cascading its runs and messages.
6. Browser drafts for that task are cleared after the API succeeds.

If runtime cleanup fails, Libre WebUI retains the task database record and
returns an error so the operator can repair the Docker or Kubernetes backend
and retry. It does not silently delete metadata while leaving an untracked
sandbox or workspace.

Stopping a run or preview is different from deletion: it stops execution but
preserves the named volume and conversation.

### Administrator demotion and user deletion

When an administrator is demoted, Libre WebUI persists the role revocation
before depending on runtime cleanup. Every later Work request checks the
current role and access mode. The backend then suspends the user's Work tasks
when the new role no longer has access and attempts to abort active runs and
stop their sandboxes. If cleanup fails, revoked access remains revoked and the
role update reports the failure so an operator can restore the runtime and
retry.

Deleting another user first removes all of that user's managed Work resources.
If external runtime cleanup fails, the user record is retained so an
administrator can retry instead of losing the ownership metadata needed for
safe cleanup.

### Back up the complete task

A complete Work backup needs both:

- the Libre WebUI database, which contains task ownership, Docker resource
  or Kubernetes resource names, provider routing, runs, messages, and activity;
  and
- every Docker volume or Kubernetes PVC labeled
  `ai.libre-webui.managed=true`, which contains the Work files.

The disposable containers and preview processes do not need to be backed up.
For a consistent backup, stop new Work activity and stop the backend before
capturing the database and task workspaces. Follow the Docker volume or
Kubernetes storage-provider snapshot procedure for the backend in use.

Restore the database and its matching workspaces together. Recreate each
volume or PVC under the exact name recorded in the database and restore its
task-ownership metadata, including `ai.libre-webui.task=<task UUID>` and
`ai.libre-webui.managed=true`. Copying only files does not preserve Docker or
Kubernetes labels. Restoring only the database produces task records whose
files are absent; restoring only storage loses the task ownership and generated
resource names that Libre WebUI uses to find and validate it.

If the installation also uses encrypted provider credentials, follow the main
Libre WebUI backup guidance for its data directory and encryption key.

## Localization and Arabic RTL

The complete Work interface is translated in all 25 supported locales:
English, Arabic, Bengali, Czech, Danish, German, Spanish, French, Hindi,
Indonesian, Icelandic, Italian, Japanese, Korean, Malay, Dutch, Polish,
Portuguese, Russian, Swedish, Thai, Turkish, Ukrainian, Vietnamese, and
Chinese.

Arabic applies `lang="ar"` and `dir="rtl"` before React renders. The sidebar
moves to the right, Conversation occupies the right side of the desktop split,
Workspace occupies the left, directional icons mirror, tab navigation follows
RTL order, and drag/keyboard resizing uses visual RTL semantics.

Technical content remains left-to-right where direction affects correctness:

- code and syntax highlighting;
- filesystem paths;
- model identifiers;
- commands and preview logs;
- tool output and metadata; and
- code-block content.

Task names, natural-language prompts, errors, filenames, and preview commands
use automatic text direction where appropriate.

## Troubleshooting

### Runtime unavailable when using `npx`

`npx libre-webui` runs the backend on the host, but it does not install Docker.
Run `docker info` as the same operating-system user that starts Libre WebUI. If
the command is absent or cannot reach the daemon, install/start Docker or fix
that user's daemon permissions, then reload Work.

Also confirm that either Ollama is healthy or at least one active
completion/chat plugin has a model and credential configured for the current
administrator.

### Runtime unavailable in Docker or Kubernetes

A repository Compose deployment should not report this: the image ships the
Docker CLI and the Compose file mounts the host socket. When it does, the panel
names the cause — a missing CLI in a custom image, a removed or absent socket
mount, or a socket group the container user is not in. For the last one, set
`DOCKER_GID` and recreate the container. See
[Running Work when Libre WebUI is itself in Docker](#running-work-when-libre-webui-is-itself-in-docker).

On Kubernetes, enable the native runtime with `--set work.enabled=true`.
Libre then reports `kubernetes`, probes the Kubernetes API, and runs sandboxes
as Pods with PVC workspaces. Do not mount a node's container-runtime socket;
see the [Kubernetes guide](./KUBERNETES).

### No Work-compatible models

For Ollama, inspect or choose a model that advertises `tools`. For a plugin,
confirm that:

- its type is completion or chat;
- it is active;
- the exact model appears in its configured model map;
- the current administrator has a usable API key; and
- the remote model implements tool calling for that provider.

Work never routes to another provider as a fallback.

### A package install or remote Git command fails

Confirm that the task's selected named runtime policy enables network access.
There is no independent per-task network toggle. Then inspect DNS, proxy,
firewall/NetworkPolicy, registry, certificate, runtime, and upstream service
configuration. Also confirm that the selected runtime image contains the
command being invoked.

The Git tab is local-only and never performs a remote operation. Use the
Terminal or model command surface only when the task's network and credential
policy deliberately permits remote Git. Do not paste a long-lived access token
into a task workspace.

### A run stops at an agent limit

The model may have exhausted the configured round or derived tool-call safety
budget. Work requests a final no-tools handoff before ending the run, so review
its completed work and remaining steps. The task remains in **Needs input**,
which is terminal for that run but deliberately does not claim completion.
Start a follow-up run to continue in the same durable workspace, or
deliberately raise `WORK_MAX_AGENT_ROUNDS` for all providers if the host and
remote-provider cost policy allow longer runs.

### HTTP 429 when starting work

The instance or administrator reached an active-runtime or persisted-task
admission limit. Wait for another run or preview to stop, delete obsolete
tasks, or deliberately raise the corresponding `WORK_MAX_*` setting for a
host with enough resources.

### The preview does not become ready

Confirm that the command remains running, binds to `0.0.0.0`, and listens on
`WORK_PREVIEW_PORT` within 15 seconds. With an empty command, Work automatically
detects a `package.json` `dev` script or a plain `index.html`, including a
single nested app. If the error reports multiple apps or no supported entry
point, enter an explicit command in the optional command field. Custom commands
start in `/workspace`, so use `cd <app-directory> && ...` for a nested app.

### The preview works on the server but not in a remote browser

Confirm that the deployment is running a build with the signed Work preview
proxy, then restart the preview to replace any legacy loopback URL. If ordinary
pages load but hot reload does not, confirm the reverse proxy and tunnel allow
WebSocket upgrades on `/api/work/previews/`. The Docker-published port should
remain on backend loopback and does not need a firewall opening.

### Files remain but the preview stopped

This is expected after cancellation, backend restart, explicit preview stop,
or failed readiness checks. The preview process is ephemeral; the named volume
is durable. Reopen the task and start the preview again.

### A file cannot be opened or saved

The integrated file API accepts UTF-8 text files up to 2 MB. If save reports
that the file changed since it was opened, reload it before editing again so
you do not overwrite another model or browser change.

Syntax highlighting intentionally switches to plain text above 8,000
characters or 400 lines. Formatting has a separate 100,000-character and
4,000-line limit and supports only the documented file families.

### Work says it is recovering sandboxes

Startup or teardown could not prove that one or more known sandboxes stopped.
Work remains fail-closed and retries every 10 seconds. Restore Docker daemon
or Kubernetes API access and inspect the backend log. Do not delete task
database rows while their labeled runtime resources still need reconciliation.

### Task deletion fails

Make sure the selected runtime is reachable. A conflicting resource without the expected
`ai.libre-webui.task` label is intentionally rejected rather than removed.
Resolve that name/ownership conflict carefully, then retry deletion.

## Security Summary

Before enabling Work for an installation, remember:

- Work is admins-only by default; opening it to all users makes every
  active account a sandbox operator, so decide deliberately. Host-folder
  workspaces stay admin-only in every mode.
- The backend must control its configured Docker daemon or Kubernetes sandbox
  namespace.
- Containers reduce filesystem exposure but are not virtual machines.
- Tasks without a named offline policy have network egress; named policies
  select the default, while destination-level restrictions remain an operator
  responsibility.
- Work volumes have no independent disk quota.
- The Git tab is local-only; remote credentials are never mounted or accepted
  by its API.
- Host firewall policy, daemon isolation, outbound restrictions, and real
  volume quotas remain operator-enforced controls.
- Remote providers receive requested tool results and can incur multiple calls
  per run.
- Preview ports stay on backend loopback and are exposed only through signed,
  revocable proxy URLs.
- Standard Docker Compose provides the Docker runtime, and Kubernetes/Helm
  provides the native Pod/PVC runtime when `work.enabled=true`.
- A complete backup requires both the Libre WebUI database and Work volumes.

## Related Docs

- [Quick Start](./QUICK_START)
- [Working with Models](./WORKING_WITH_MODELS)
- [Keyboard Shortcuts](./KEYBOARD_SHORTCUTS)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Docker](./DOCKER)
- [Kubernetes](./KUBERNETES)
- [Authentication](./AUTHENTICATION)
- [Troubleshooting](./TROUBLESHOOTING)

---

# MLX LM on Apple Silicon

URL: https://docs.librewebui.org/mlx-apple-silicon

# MLX LM on Apple Silicon

Libre WebUI includes an **MLX LM (Apple Silicon)** plugin for running
MLX-formatted language models directly on an M-series Mac. The plugin connects
to the OpenAI-compatible HTTP API built into
[MLX LM](https://github.com/ml-explore/mlx-lm).

This path is useful when you want native Metal inference without converting an
MLX checkpoint to an Ollama or GGUF model.

## Architecture

```text
Libre WebUI in native development mode
  frontend http://localhost:5173
  backend  http://localhost:3001
                  |
                  | OpenAI-compatible chat request
                  v
mlx_lm.server http://127.0.0.1:8081
                  |
                  v
MLX model on Apple Silicon unified memory
```

Port `8081` is intentional. MLX LM normally defaults to `8080`, which conflicts
with the packaged `npx libre-webui` server.

## Requirements

- An Apple Silicon Mac (M1 or newer).
- macOS with the Xcode command-line tools available.
- Python 3.10 or newer.
- Enough unified memory for the selected model, its KV cache, and macOS.
- Libre WebUI running natively. The source development workflow is the simplest
  setup because both backends can use the Mac loopback interface.

The default Ternary Bonsai model is about 8.5 GB on disk and needs more memory
while running. A Mac with 16 GB unified memory can handle shorter contexts, but
24 GB or more leaves more practical headroom. Use a smaller MLX checkpoint and
add its repository ID to a copied plugin definition if memory is tight.

## Install MLX LM

Using uv keeps the command isolated from Homebrew Python packages:

```bash
brew install uv
uv tool install --upgrade mlx-lm
rehash
mlx_lm.server --help
```

If the tool already exists:

```bash
uv tool upgrade mlx-lm
rehash
```

Qwen 3.5 models require `mlx-lm` 0.30.7 or newer. The repository example
requires 0.31.3 or newer.

## Start the Server

For the Ternary Bonsai model:

```bash
mlx_lm.server \
  --model "prism-ml/Ternary-Bonsai-27B-mlx-2bit" \
  --host 127.0.0.1 \
  --port 8081 \
  --max-tokens 262144 \
  --allowed-origins "http://localhost:5173,http://127.0.0.1:5173"
```

The first run downloads the model from Hugging Face. Later runs use the local
cache. Ternary Bonsai declares `262144` maximum positions. The prompt and
generated output share that context window, so a long prompt reduces the number
of tokens that can be generated even though the server allowance is set to the
[model maximum](https://huggingface.co/prism-ml/Ternary-Bonsai-27B-mlx-2bit/blob/main/config.json).

For a smaller starter model:

```bash
mlx_lm.server \
  --model "mlx-community/Llama-3.2-3B-Instruct-4bit" \
  --host 127.0.0.1 \
  --port 8081 \
  --max-tokens 2048
```

The repository also contains a reusable launcher:

```bash
cd examples/mlx-lm-server
uv run server.py
```

Inspect the resolved command without loading a model:

```bash
uv run server.py --dry-run
```

## Verify the OpenAI-Compatible API

Check health and model discovery:

```bash
curl http://127.0.0.1:8081/health
curl http://127.0.0.1:8081/v1/models
```

Send a non-streaming chat request:

```bash
curl http://127.0.0.1:8081/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "prism-ml/Ternary-Bonsai-27B-mlx-2bit",
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "Reply with: MLX is ready."}
    ],
    "temperature": 0.7,
    "top_p": 0.95,
    "max_tokens": 64,
    "stream": false
  }'
```

The server also supports streaming Server-Sent Events when `"stream": true`.

## Connect Libre WebUI

From the Libre WebUI repository root:

```bash
npm install
npm run dev
```

Open [http://localhost:5173](http://localhost:5173), then:

1. Open **Settings > Plugins**.
2. Find **MLX LM (Apple Silicon)**.
3. Confirm the endpoint is
   `http://127.0.0.1:8081/v1/chat/completions`.
4. Activate the plugin. Local MLX does not require an API key.
5. Return to Chat and select the MLX model.

The built-in model list includes:

- `prism-ml/Ternary-Bonsai-27B-mlx-2bit`

The model selected in Libre WebUI must match a model available to the MLX
server. To use another checkpoint, export or copy `plugins/mlx-lm.json`, add the
repository ID to `model_map`, and import the edited definition from
**Settings > Plugins**.

## Generation Settings

Ternary Bonsai's published recommendations are:

| Setting     | Value  |
| ----------- | ------ |
| Temperature | `0.7`  |
| Top P       | `0.95` |
| Top K       | `20`   |

Libre WebUI sends temperature and Top P through the plugin. Start the server
with `--top-k 20` when you want its Top K recommendation:

```bash
mlx_lm.server \
  --model "prism-ml/Ternary-Bonsai-27B-mlx-2bit" \
  --host 127.0.0.1 \
  --port 8081 \
  --top-k 20 \
  --max-tokens 262144
```

## Work and Tool Calling

The MLX plugin can appear in Work because it uses the OpenAI-compatible chat
format. Choose it for Work only when the model and its chat template reliably
support OpenAI-style tool calls. Ordinary text generation working in Chat does
not prove that a checkpoint supports tools.

Tool parsers and model templates change quickly. If a Work run returns malformed
tool calls, update `mlx-lm`, test the same tool request directly against the
server, and try a model whose MLX card explicitly documents tool use.

## Docker Networking

Native Libre WebUI development is recommended. A container cannot reach the
Mac's `127.0.0.1`.

If Libre WebUI runs in Docker:

1. Start MLX LM with `--host 0.0.0.0`.
2. Use a private Mac LAN address such as
   `http://192.168.1.20:8081/v1/chat/completions` as the plugin endpoint.
3. Allow port `8081` only on trusted local networks.

Do not expose `mlx_lm.server` directly to the public internet. Its maintainers
describe it as a local server with only basic security checks. Put an
authenticated HTTPS reverse proxy in front of it for any non-local deployment.

## Troubleshooting

**`Model type qwen3_5 not supported`**

An older launcher is still being used:

```bash
rehash
which -a mlx_lm.server
uv tool upgrade mlx-lm
```

**Libre WebUI shows the model but requests fail**

Verify the same model ID works directly:

```bash
curl http://127.0.0.1:8081/v1/models
```

Then confirm the plugin endpoint includes `/v1/chat/completions`.

**Address already in use**

Keep Libre WebUI on its normal port and move MLX:

```bash
mlx_lm.server --model "owner/model" --port 8082
```

Update the plugin endpoint to
`http://127.0.0.1:8082/v1/chat/completions`.

**The model is slow on its first request**

Initial loading and prompt prefill are more expensive than token-by-token
generation. Watch Activity Monitor's memory pressure and choose a smaller model
or shorter conversation if macOS starts swapping.

## Related Docs

- [Plugins](./PLUGIN_ARCHITECTURE)
- [Working with Models](./WORKING_WITH_MODELS)
- [Hardware Requirements](./HARDWARE_REQUIREMENTS)
- [Work: Isolated Workspaces](./WORKSPACES)

---

# Connect Third-Party and Self-Hosted Providers

URL: https://docs.librewebui.org/provider-connections

# Connect Third-Party and Self-Hosted Providers

Libre WebUI 0.16.0 adds a focused **Provider connections** workspace inside
**Settings > Plugins**. Use it to activate a bundled provider, point a
compatible plugin at another API, inspect the effective model catalog, or
connect a self-hosted gateway on a trusted network.

![Libre WebUI Provider connections with provider search and selection, connection controls, model refresh, and a provider-qualified capability catalog.](./assets/provider-connections-0.16.0.png)

Libre WebUI currently ships support for these provider wire formats:

- OpenAI Chat Completions;
- OpenAI Responses;
- Anthropic Messages; and
- Google Gemini contents and function calling.

The bundled Anthropic and Gemini definitions use dedicated adapters selected by
their provider identities. A newly imported provider uses OpenAI Chat
Completions or OpenAI Responses semantics; pointing it at an Anthropic- or
Gemini-compatible API does not select those bundled adapters. A provider with
another request, streaming, tool-call, or response shape needs a backend
adapter. Plugin JSON describes routing and configuration; it does not translate
an unrelated protocol.

## Open Provider Connections

1. Sign in and open **Settings > Plugins**.
2. Search the provider list in the left pane.
3. Select a provider to review its active state and effective model catalog.
4. Activate the provider for your account.
5. Select **Configure** only when you need to save a credential or override a
   connection setting.

Provider configuration is closed by default. Connection settings appear first
for administrators, while sampling controls such as temperature and token
limits remain under the separately collapsed **Advanced parameters** section.
Inherited defaults appear as hints rather than pre-filled account overrides.

Plugin definitions are shared instance configuration, so only administrators
can import, install, update, or delete them. Each authenticated user controls
their own activation state, credential, and allowed generation settings.

## Add a Connection Quickly

**Settings > Connections** is a shorter path for the common case: one
OpenAI-compatible endpoint, one API key. Administrators see a card for the
local Ollama runtime with its health and version, a list of the existing
OpenAI-compatible connections, and a small form to add another.

Adding a connection takes a display name, the full chat completions URL, and
an optional API key. Libre WebUI derives the connection ID from the name,
installs the provider definition, stores the key server-side, activates the
connection, and asks the endpoint which models it serves. The discovered
models replace the placeholder catalog and appear in the chat model picker.

Each row carries the endpoint, the model count, whether a key is stored, an
active toggle, a model refresh, and a delete. Anything beyond this — response
API modes, base URL overrides, per-capability catalogs, generation parameter
policy — still lives in the fuller **Settings > Plugins** workspace described
above.

## Codex (ChatGPT Sign-In)

The bundled **Codex (ChatGPT)** provider needs no API key. When the server has
a Codex CLI sign-in (`codex login` as the server's operating-system user), the
provider appears to administrators, offering the documented Codex model family
through the ChatGPT session. Access tokens are read from the CLI's own
`auth.json`, refreshed through the same OAuth client the CLI uses, and written
back so the CLI keeps working; token values never appear in logs.

Because the requests are made by the backend — never from inside a task
container — these models also power Work with the normal sandboxed tool loop.
The provider is administrator-only since every call spends the server owner's
ChatGPT subscription. Hide it entirely with `CODEX_OAUTH_MODELS_ENABLED=false`,
or point at a different sign-in with `CODEX_HOME`.

## Choose a Bundled or Imported Provider

Libre WebUI includes definitions for OpenAI, Anthropic, Gemini, Groq, Mistral,
OpenRouter, Kimi Code by Moonshot AI, Hugging Face, GitHub Models, local MLX LM,
and other model or media services. Start with a bundled entry when its protocol
and authentication contract match the service you want to use.

For another compatible service, an administrator can import a plugin JSON
definition. This minimal example describes an OpenAI-compatible gateway:

```json
{
  "id": "private-ai-gateway",
  "name": "Private AI Gateway",
  "type": "completion",
  "endpoint": "http://ai-gateway:8080/v1/chat/completions",
  "api_mode": "chat_completions",
  "auth": {
    "header": "Authorization",
    "prefix": "Bearer ",
    "key_env": "PRIVATE_AI_GATEWAY_API_KEY"
  },
  "model_map": ["gateway-chat"]
}
```

Import the file from **Settings > Plugins**, activate it, and save the API key
for the account that will use the connection. Add connection variables to the
definition when administrators need editable Base URL, path, discovery, or
capability-specific endpoint fields. The bundled
[`plugins/openai.json`](https://github.com/libre-webui/libre-webui/blob/main/plugins/openai.json)
is a complete example.

For an intentionally authless gateway on a trusted network, set both
`auth.header` and `auth.key_env` to empty strings and omit `auth.prefix`. Libre
WebUI then does not require or send an API key for that plugin.

## Choose Chat Completions or Responses

OpenAI-compatible completion plugins can use either API mode:

| API mode           | Default request path | Typical request field |
| ------------------ | -------------------- | --------------------- |
| `chat_completions` | `/chat/completions`  | `messages`            |
| `responses`        | `/responses`         | `input`               |

The bundled OpenAI provider exposes **API Mode** in its configuration. Libre
WebUI maps completed and streamed Responses output back into Chat and Work,
including bounded replay state for reasoning and tool calls.

Changing the mode affects the default operation path. It does not change the
protocol spoken by the upstream server, so select Responses only when that
server implements compatible Responses request and event shapes.

## Configure a Base URL or Full Endpoint

Libre WebUI resolves a completion route in this order:

1. A non-default full `endpoint` override.
2. `base_url` plus an optional `api_path`.
3. The endpoint declared by the plugin definition.

Use **Base URL** for the API root:

```text
https://gateway.example/v1
```

With no custom path, Chat Completions mode sends requests to:

```text
https://gateway.example/v1/chat/completions
```

Responses mode instead sends them to:

```text
https://gateway.example/v1/responses
```

Use **API Path** when the provider exposes a compatible operation at another
path relative to that root. Use **Legacy Full Endpoint** only when you need to
provide the complete operation URL; a genuine full endpoint takes precedence
over Base URL and API Path.

Known `/chat/completions`, `/completions`, and `/responses` endpoint suffixes
also identify the request semantics. A custom, unrecognized operation path
keeps the explicitly selected API mode.

After a route or API-key change, save the provider again before testing Chat.
When the plugin declares authentication, a custom connection route requires a
credential saved by the same account. An intentionally authless plugin can
leave both authentication fields empty. Libre WebUI does not send an
operator-managed environment key to a user-defined destination; environment
fallback is reserved for the trusted bundled route.

## Discover or Maintain Model IDs

Select an active chat provider and use **Refresh models** to run discovery.
Libre WebUI reloads both the selected provider's catalog and Chat's model list.

Discovery also runs without being asked: an active provider's catalog is
rediscovered when it is missing or has aged past
`PLUGIN_MODEL_DISCOVERY_TTL_MS`, so the models you see follow the provider
rather than the moment you activated it. **Refresh models** forces a check
immediately and reports what happened:

| Result                      | Meaning                                                                     |
| --------------------------- | --------------------------------------------------------------------------- |
| Catalog updated             | The provider answered and its model list differs from the stored one        |
| Catalog already up to date  | The provider answered with the same list                                    |
| API key needed              | No usable key, so no request was made — the previous catalog is still shown |
| Catalog could not be loaded | The provider was unreachable or returned nothing usable                     |

A key set only in the environment is not used for a provider that runs an
installed definition instead of the bundled one; the message says so when that
applies. Speech, image, and embedding models found in a provider's catalog are
listed here with their capability labels, but are kept out of the chat model
picker.

For an OpenAI-compatible route, discovery chooses the model-list URL as
follows:

- a route ending in `/models` is used as-is;
- a known operation suffix such as `/chat/completions`, `/completions`,
  `/responses`, `/embeddings`, or `/messages` is replaced with `/models`; and
- otherwise, `/models` is appended to the route.

For example, both of these completion routes derive the same discovery URL:

```text
https://gateway.example/v1/chat/completions
https://gateway.example/v1/responses

-> https://gateway.example/v1/models
```

When derivation cannot produce the correct full URL, expose
`models_endpoint` in the plugin's `variables` array:

```json
{
  "name": "models_endpoint",
  "type": "string",
  "label": "Models Endpoint",
  "default": "https://gateway.example/v1/models"
}
```

The inherited default or administrator-saved value takes precedence over the
derived address. A top-level `models_endpoint` manifest property is not read.
Discovery expects an OpenAI-compatible response with model objects in a `data`
array:

```json
{
  "data": [{ "id": "gateway-chat" }, { "id": "gateway-code" }]
}
```

Discovered IDs are stored per user and do not rewrite the shared plugin file.
If the provider does not implement compatible discovery, maintain fallback
model IDs in the plugin JSON `model_map`. The catalog in Provider connections
is read-only; capability labels describe which plugin route lists a model and
are not health checks.

Model IDs are not globally unique. Chat stores the raw model ID together with
its exact Ollama or plugin provider identity, so an Ollama model and multiple
plugins can safely expose the same name. If the saved provider becomes
unavailable, Libre WebUI shows that selection as unavailable instead of
silently routing the request to another provider.

## Configure Image Generation Separately

The bundled OpenAI provider exposes image generation through
`https://api.openai.com/v1/images/generations` and currently defaults new
configurations to `gpt-image-2`. Older GPT Image IDs remain in its fallback
catalog for compatible existing deployments.

Chat and image routes are intentionally isolated. A custom Chat Base URL does
not automatically receive image requests. Leave `image_endpoint` blank to use
the image endpoint declared by the plugin, or set it to the complete compatible
Image API operation URL when your provider supplies one.

Image choices are provider-qualified, just like Chat choices. If two active
plugins expose the same image model ID, Libre WebUI sends the request only to
the provider selected in the image panel.

## Connect an HTTP Gateway Safely

Provider endpoints can use absolute HTTP or HTTPS URLs. HTTP is useful for a
self-hosted gateway on a trusted LAN, Tailscale network, or private container
network, but it sends API keys, prompts, tool results, and generated content
without transport encryption. Prefer HTTPS whenever the route crosses a
network boundary or the gateway supports TLS.

Requests originate from the Libre WebUI backend, not from the browser. Choose
an address that is reachable from that backend:

| Backend location              | Example provider root                 |
| ----------------------------- | ------------------------------------- |
| Native process, same machine  | `http://127.0.0.1:8081/v1`            |
| Docker Compose service        | `http://ai-gateway:8080/v1`           |
| Container to supported host   | `http://host.docker.internal:8081/v1` |
| Trusted LAN or Tailscale host | `http://192.168.1.20:8081/v1`         |

Inside a container, `localhost` identifies the Libre WebUI container itself.
It does not identify another Compose service or automatically reach the host.

Libre WebUI accepts only HTTP and HTTPS provider URLs, validates the final
destination before selecting a credential, and does not follow redirects for
provider or discovery requests. Configure the final operation URL directly.

## Verify the Gateway Before Activating It

Test model discovery from the machine or container that runs the Libre WebUI
backend:

```bash
curl http://ai-gateway:8080/v1/models \
  -H 'Authorization: Bearer YOUR_GATEWAY_KEY'
```

Then test the operation matching the selected API mode.

Chat Completions:

```bash
curl http://ai-gateway:8080/v1/chat/completions \
  -H 'Authorization: Bearer YOUR_GATEWAY_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gateway-chat",
    "messages": [{"role": "user", "content": "Reply with: ready"}],
    "stream": false
  }'
```

Responses:

```bash
curl http://ai-gateway:8080/v1/responses \
  -H 'Authorization: Bearer YOUR_GATEWAY_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gateway-chat",
    "input": "Reply with: ready",
    "store": false
  }'
```

After both calls work, configure the same route, mode, credential, and model ID
in Provider connections. Activate the provider, select **Refresh models**, and
then choose its provider-qualified model in Chat. Work can also use it when the
model reliably supports tool calling.

## Troubleshooting

| Symptom                                     | Check                                                                                                                   |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Requests still reach the bundled endpoint   | Remove a stale full endpoint override, then save the intended Base URL and API Path.                                    |
| The provider receives the wrong payload     | Match API Mode to the upstream Chat Completions or Responses protocol and verify the final suffix.                      |
| Refresh models returns no IDs               | Test `/models`, verify the `data[].id` shape, expose/configure the `models_endpoint` variable, or maintain `model_map`. |
| A previous model remains after a route edit | Save the connection change; Libre WebUI clears that user's obsolete discovered catalog before refreshing.               |
| The API key is reported missing             | Save a per-user credential for the custom route; bundled environment fallback does not follow overrides.                |
| A Docker deployment cannot reach localhost  | Use the gateway's Compose service name, a supported host alias, or a reachable private-network address.                 |
| Chat works but image generation does not    | Configure the separate complete `image_endpoint` and select a model exposed by that image capability.                   |
| Chat works but Work rejects the model       | Confirm the model supports compatible tool calls; ordinary text completion is not sufficient.                           |
| The provider returns a redirect             | Configure the final validated URL directly; Libre WebUI intentionally does not follow provider redirects.               |

For detailed routing, credential, replay-state, and authorization behavior,
read [Plugins](./PLUGIN_ARCHITECTURE). For deployment-specific failures, see
[Troubleshooting](./TROUBLESHOOTING).

## Community Acknowledgment

This guide and Libre WebUI 0.16.0's Provider connections experience were
shaped by [ZhengJin (@fangzhengjin)](https://github.com/fangzhengjin), whose
detailed third-party-provider feedback and AI-assisted UX concept in
[#163](https://github.com/libre-webui/libre-webui/issues/163) helped define the
workflow.

## Related Docs

- [Plugins](./PLUGIN_ARCHITECTURE)
- [Working with Models](./WORKING_WITH_MODELS)
- [MLX LM on Apple Silicon](./MLX_APPLE_SILICON)
- [Work: Isolated Workspaces](./WORKSPACES)
- [Troubleshooting](./TROUBLESHOOTING)

---

# Use an Installed Coding Agent as a Chat Model

URL: https://docs.librewebui.org/agent-cli-models

# Use an Installed Coding Agent as a Chat Model

If you already pay for a coding agent and it is signed in on the machine running
Libre WebUI, you can talk to it from a normal chat instead of adding a separate
API key. Libre WebUI detects the agent's command-line interface and offers it in
the model selector like any other model.

## What gets detected

On each request for the model list, Libre WebUI looks on the server's `PATH` for
these commands:

| Model           | Command    | Provided by   |
| --------------- | ---------- | ------------- |
| **Claude Code** | `claude`   | Anthropic     |
| **Codex**       | `codex`    | OpenAI        |
| **OpenCode**    | `opencode` | SST           |
| **Pi**          | `pi`       | Mario Zechner |

Anything found appears under an **Agents** group in the model selector. Nothing
is installed for you, and no configuration file is required — if the command
runs in your server's shell, it shows up.

## Choosing a model, not just a CLI

Each CLI can expose several entries in the Agents group:

- **Claude Code** offers its signed-in default plus Sonnet, Opus, and Haiku.
- **Codex** offers its configured default plus the documented ChatGPT sign-in
  family: GPT-5.6 Sol, Terra, Luna, and GPT-5.3 Codex Spark.
- **Pi** runs with the model configured in the CLI itself.
- **OpenCode** lists the models of every provider it is authenticated with
  (from `opencode models`), and always requires an explicit choice — its
  CLI-level default can point at a local server that is not reachable from the
  Libre WebUI host.

Pi runs each turn stateless (`--no-session`), with local tools disabled and a
neutral system prompt, so replies are not colored by — and chats never touch —
the personal Pi configuration of the server's operating-system user.

## Using it

1. Sign in as an administrator.
2. Start a chat and open the model selector.
3. Pick an entry under **Agents**.
4. Send messages as usual. Replies stream back token by token.

The conversation is stored like any other chat, so you can leave and come back
to it, rename it, and search it from the command palette. Each turn sends a
transcript of the recent conversation to the agent and streams its answer back.

## Who can use it, and what it can reach

This feature is **administrator-only**, and the reason matters. The agent runs
as the same operating-system user as the Libre WebUI backend, on the host — not
inside a Work container. It therefore inherits that user's agent credentials and
whatever access those agents normally have, including the ability to read files
and run commands on the server if it decides to.

Treat enabling this as equivalent to granting the agent shell access to the
machine. Because of that, the feature ships **disabled**: an administrator must
turn it on under **User Management → Agents**. The setting is persisted and
takes effect immediately, without a restart.

To pin the decision at the deployment level regardless of the runtime toggle,
set the environment variable either way (this also locks the toggle in the
interface):

```bash
AGENT_CLI_MODELS_ENABLED=false   # or true
```

For a model that can act on files but stays inside a sandbox, use
[Work](./WORKSPACES) instead: it runs tool-capable models in a locked-down
container with an isolated workspace.

## Configuration

| Variable                   | Default  | Purpose                                                                           |
| -------------------------- | -------- | --------------------------------------------------------------------------------- |
| `AGENT_CLI_MODELS_ENABLED` | unset    | Pin the feature `true`/`false`; unset defers to the admin toggle (off by default) |
| `AGENT_CLI_TIMEOUT_MS`     | `600000` | How long a single agent turn may run before it is killed                          |

## Troubleshooting

**No Agents group appears.** Confirm the feature is enabled under
**User Management → Agents** (it is off by default) and that you are signed in
as an administrator, then
check that the command is on the `PATH` of the process running the backend — not
just your interactive shell. A service manager, Docker container, or desktop
launcher often starts with a much smaller `PATH` than a login terminal.

**The reply fails immediately.** Run the same command by hand as the server user
(`claude -p "hello"` or `codex exec "hello"`). Most failures are the agent asking
for a login that has expired, or a rate limit on the underlying subscription.
OpenCode in particular reports an expired provider login only in its own logs;
re-run `opencode auth login` as the server user.

**Replies stop partway.** A long turn may have hit `AGENT_CLI_TIMEOUT_MS`. Raise
it, or break the request into smaller steps.

## Related

- [Work: Isolated Workspaces](./WORKSPACES)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Connect Third-Party and Self-Hosted Providers](./PROVIDER_CONNECTIONS)

---

# Private Remote Deployment

URL: https://docs.librewebui.org/private-remote-deployment

# Private Remote Deployment

This pattern runs Libre WebUI, Ollama, and Cloudflare Tunnel on one Docker host
without publishing the application or Ollama ports. Cloudflare Access is the
outer identity boundary; Libre WebUI authentication remains the inner boundary.
Work and Watchtower are separate, root-equivalent opt-ins.

This template is the single-replica `solo` topology: SQLite, local encrypted
blobs, embedded vectors, local coordination, and an embedded job worker share
the application data volume. Do not turn it into a team deployment by changing
backend selectors in `.env`. Team deployments must use the repository's
`docker-compose.team.yml` (and `docker-compose.team.work.yml` when Work is
enabled), which provisions PostgreSQL/PGVector, versioned S3 storage, Redis,
an external worker, and the gateway as one coordinated topology.

Use [`deploy/private/docker-compose.yml`](https://github.com/libre-webui/libre-webui/blob/main/deploy/private/docker-compose.yml)
as the starting point. It defaults to the `main` image:

```env
LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui:main
```

The `dev` tag is suitable for an explicitly opted-in development instance, not
the client default.

## Security model

- Cloudflare Access protects the entire hostname, including `/api/*` and
  WebSocket upgrades. Do not add public bypass paths.
- Libre WebUI requires a current account for application APIs. Model lifecycle
  and Work operations require the current database role to be administrator.
- The application, Ollama, SearXNG, and cloudflared only use a private Compose
  network. The host publishes no application ports.
- The bundled SearXNG service powers optional [web search](./WEB_SEARCH). It
  is internal-only and inert until an administrator enables search in
  Settings > Search; set `SEARXNG_SECRET` in `.env` before starting the
  stack.
- The app runs non-root with a read-only root filesystem, no Linux
  capabilities, no-new-privileges, and CPU, memory, and PID limits.
- Work is disabled unless one of its overrides is included. When enabled,
  its containers add their own read-only root filesystem, capability drop,
  resource limits, workspace volume, and default-deny network policy.

The base stack mounts no Docker socket. Enabling Work with
`docker-compose.work-proxy.yml` keeps it that way: a socket proxy on an
internal network holds the socket and forwards only the API sections Work
uses (containers, images, volumes, networks, exec, info); swarm, secrets,
build, and system endpoints are denied at the proxy, and the application
needs no socket mount or socket-group membership. The proxy narrows the
Docker API surface, not the blast radius of what it forwards — whoever can
create containers can still bind-mount host paths — so treat it as a real
hardening layer, not as multi-tenant isolation.

The raw-socket alternatives remain the largest trust boundary: the
`docker-compose.work.yml` and Watchtower overrides give a container a
process that can issue arbitrary Docker API calls, which can control the
host. A read-only socket mount does not make Docker API access read-only. The
integrated backup helper refuses to inherit a raw Docker socket; migrate Work
to the filtered proxy before relying on scheduled integrated backups.

## Bootstrap

1. Create a non-root sudo operator and verify key-based SSH login before
   disabling root SSH.
2. Copy `deploy/private/.env.example` to `/opt/libre-webui/.env`, set mode
   `0600`, generate unique secrets, and size `BLOB_QUOTA_BYTES_PER_USER` for
   the host. `BLOB_QUOTA_RESERVATION_TTL_MS` expires abandoned upload
   reservations; its default is one hour.
3. If Work will be enabled, set `DOCKER_GID` to the numeric group that owns
   `/var/run/docker.sock`.
4. Store the Cloudflare tunnel token in
   `/opt/libre-webui/secrets/tunnel-token` with mode `0640` or stricter.
5. Create a Cloudflare Access self-hosted application for the complete
   hostname, use a 24-hour session, and allow only the intended identities.
   Enable **Protect with Access** on the tunnel route. If monitoring requires a
   public health check, create a separate path-scoped application or policy for
   `/health/live` only. Never add a blanket Bypass policy to the main application:
   matching Bypass policies defeat its Allow policy.
6. Leave `ENABLE_SIGNUP=false`. Once the Access allowlist protects the hostname,
   create the first local administrator; an empty database permits that one
   bootstrap account automatically. Enable registration only for a deliberate
   later registration window.
7. Configure Turnstile hostname restrictions and set
   `TURNSTILE_EXPECTED_HOSTNAME` to the exact public hostname.

Start and verify:

```bash
cd /opt/libre-webui
docker compose config --quiet
docker compose up -d
docker compose ps
```

To enable Work, include the socket-proxy override deliberately:

```bash
docker compose -f docker-compose.yml -f docker-compose.work-proxy.yml up -d
```

The raw-socket variant (`docker-compose.work.yml`) remains available for
deployments that need it, with the trust consequences described above.

Once Access is active, command-line smoke tests need a Cloudflare Access
service token unless the exact path has a narrow bypass. Store the credentials
outside shell history and send both headers:

```bash
curl --fail --silent --show-error \
  -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
  -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
  https://your-hostname.example/api/auth/system-info
```

An unauthenticated request to a protected application API must return `401`:

```bash
curl --output /dev/null --write-out '%{http_code}\n' \
  -H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
  -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
  https://your-hostname.example/api/work/tasks
```

## Host hardening

The directory includes an sshd drop-in and fail2ban jail. Before applying the
sshd drop-in, verify a separate non-root sudo session in another terminal. Test
configuration with `sshd -t` before reloading SSH.

Use UFW (or an equivalent firewall) to default-deny inbound traffic and permit
only rate-limited SSH. Docker publishes no service ports in this template:

```bash
ufw default deny incoming
ufw default allow outgoing
ufw limit OpenSSH
ufw enable
```

Keep unattended security upgrades enabled. Disable X11, agent, and TCP
forwarding unless the deployment has a documented need for them.

## Backups and recovery

Before taking a backup, run the read-only recovery inventory inside the running
deployment container. This uses the exact deployed application version,
environment, and mounted data volume. A command from a host checkout can inspect
the wrong database or run source that differs from the deployed image.

```bash
docker exec libre-webui \
  libre-webui recovery-check --json --data-dir /app/backend/data
```

Exit status `0` means no recovery-readiness blockers were found, `1` means the
JSON report contains blockers, and `2` means the command could not run. The
report includes only an encryption-key fingerprint and secret-presence flags;
it never prints a key or other secret value. Keep the inventory with the
corresponding backup so operators can compare the application version, schema
fingerprint, expected Work resources, and exclusions before a restore.

Create dedicated backup encryption and signing keys with the exact deployed
image. Keep this directory off the application volume and copy the encryption
key and signing private key to a separate protected recovery location:

```bash
install -d -m 0700 /etc/libre-webui/backup-keys
image_ref=$(docker inspect libre-webui --format '{{.Image}}')
docker run --rm --user 0:0 --read-only --network none --cap-drop ALL \
  --security-opt no-new-privileges \
  --mount type=bind,src=/etc/libre-webui/backup-keys,dst=/backup-keys \
  --entrypoint /usr/local/bin/libre-webui "$image_ref" \
  backup keygen \
  --directory /backup-keys
```

Key generation refuses existing output files. Never generate new keys over an
existing backup set: losing either the archive encryption key or the signing
identity makes the corresponding recovery proof unusable.

Install the provided backup and restore scripts and systemd units, then enable
the timer:

```bash
install -d -m 0700 /var/backups/libre-webui
install -m 0750 deploy/private/libre-webui-backup \
  /usr/local/sbin/libre-webui-backup
install -m 0750 deploy/private/libre-webui-restore \
  /usr/local/sbin/libre-webui-restore
install -m 0644 deploy/private/libre-webui-backup.{service,timer} \
  /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now libre-webui-backup.timer
```

The unit optionally reads maintenance-only overrides from
`/etc/libre-webui/backup.env`; it does not load the application `.env`. Create
the file as root only when an override is needed:

```bash
install -d -m 0750 /etc/libre-webui
install -m 0600 /dev/null /etc/libre-webui/backup.env
```

`LIBRE_WEBUI_STACK_DIR`, `LIBRE_WEBUI_BACKUP_RETENTION_DAYS`,
`LIBRE_WEBUI_CONTAINER_NAME`, and `LIBRE_WEBUI_BACKUP_KEY_DIR` can be set there
directly. Keep the file owned by root and mode `0600`. A custom key directory
must remain readable by root inside the systemd sandbox.

Changing `LIBRE_WEBUI_BACKUP_DIR` also changes the systemd write boundary. The
directory must exist before the service starts, and the unit needs a matching
drop-in. For example, after setting
`LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui` in `backup.env`:

```bash
install -d -m 0700 /srv/backups/libre-webui
systemctl edit libre-webui-backup.service
```

Add this exact path in the editor, then reload the unit:

```ini
[Service]
ReadWritePaths=/srv/backups/libre-webui
```

```bash
systemctl daemon-reload
systemctl start libre-webui-backup.service
```

Without the matching `ReadWritePaths=` entry, `ProtectSystem=strict` correctly
prevents the timer from writing to a custom location.

The backup service allows up to six hours for large archives. The helper
acquires a host lock, stops the application only when it was already running,
and creates the archive against the quiesced volume with the exact deployed
image. The archive has a signed manifest and an operator-encrypted payload; it
includes the data directory plus the runtime and secret configuration required
to open that state. The helper then independently verifies the complete
archive before atomically publishing its metadata report. Its read-only
maintenance containers receive a private writable `/tmp` tmpfs for SQLite
inspection and authenticated archive verification; no temporary plaintext is
persisted in the container layer. Copy both files and the separately protected
recovery keys off-host.

When Work uses `docker-compose.work-proxy.yml`, recovery must also prove that
every database-referenced Work volume still exists. The helper reads the
deployed application's `DOCKER_HOST`, locates the socket-proxy service in the
same live Compose project, and discovers their one shared internal network from
Docker's actual network attachments. Compose prefixes that network with the
project name, so do not configure or hardcode a guessed network name. Only the
archive-creation container joins that internal network and can reach the
filtered proxy; it receives no raw socket. Independent archive verification
continues with `--network none`. A missing proxy, an unexpected endpoint, an
external or ambiguous shared network, or a raw-socket mount fails before the
application is stopped and before an archive is published.

Test recovery into a new volume without replacing the live volume:

```bash
LIBRE_WEBUI_RESTORE_IMAGE="$image_ref" \
  libre-webui-restore \
  /var/backups/libre-webui/libre-webui-integrated-YYYYMMDDTHHMMSSZ.lwb \
  libre-webui-restore-drill
```

The restore helper refuses an existing volume or configuration target, verifies
the archive and its internal recovery inventory in disposable storage, then
copies data into the new volume and writes recovered `runtime.json` and
`secrets.json` with private permissions. It never rewires or starts the live
stack. Inspect the recovered configuration, update deployment-specific values
deliberately, and test the restored volume with an isolated stack.

Ollama models can be pulled again. Docker Work volumes, Kubernetes Work PVCs,
and host-bound Work folders are outside the application data directory and
require their own coordinated snapshots and retention policy.

## Updates

Libre WebUI is stateful even when its image tag is mutable. The base Compose
file permanently labels the application as excluded from Watchtower. Upgrade
it only as a coordinated operator action:

1. Record the running image ID and resolve the reviewed replacement to an
   immutable digest.
2. Run `libre-webui recovery-check`, start the backup service, and require a
   newly created archive and verification report before continuing.
3. Set `LIBRE_WEBUI_IMAGE` to the reviewed digest, pull it, and recreate only
   `libre-webui` with Docker Compose. Do not remove or recreate its data volume.
4. Require `/health/ready`, sign-in, session/history, document retrieval, and
   Work smoke tests to pass. Roll back to the recorded image digest if they do
   not; preserve both the failed state and the verified backup for diagnosis.

The host-side sequence is intentionally manual. Replace the digest only after
reviewing it, and inspect the newest `.lwb` and `.json` pair before the pull:

```bash
docker inspect libre-webui --format '{{.Config.Image}} {{.Image}}'
docker exec libre-webui \
  libre-webui recovery-check --json --data-dir /app/backend/data
systemctl start libre-webui-backup.service
systemctl --no-pager --full status libre-webui-backup.service
ls -lt /var/backups/libre-webui/libre-webui-integrated-* | head

# Set LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui@sha256:REVIEWED_DIGEST
# in the root-owned .env, then recreate only the application.
docker compose pull libre-webui
docker compose up -d --no-deps libre-webui
docker inspect libre-webui --format '{{.State.Health.Status}} {{.Image}}'
```

The optional socket-bearing Watchtower override remains available only for the
sidecars explicitly labelled in the base file:

```bash
docker compose \
  -f docker-compose.yml \
  -f docker-compose.watchtower.yml \
  up -d
```

Watchtower checks Ollama and SearXNG every 30 minutes. Ollama model data remains
in its named volume, and SearXNG configuration remains in its bind mount. It
does not update Libre WebUI, cloudflared, the Work socket proxy, or Work
sandboxes. A client deployment follows `main`; an experimental instance may
select `:dev`, but the application still requires the same backup-gated manual
upgrade. Never attach this private solo stack to team persistence services;
deploy the complete team topology instead.

---

# System Diagnostics & Usage Analytics

URL: https://docs.librewebui.org/system-monitoring

# System Diagnostics & Usage Analytics

Libre WebUI 0.18.0 gives administrators two live views of the instance: a
**System** page with host and runtime diagnostics, and a **Usage** page with
provider usage analytics. Both are administrator-only, in the backend and in
the interface, and both read only the instance's own state — nothing is
transmitted off the box.

Reach them from the sidebar admin entries, the tab menu's admin shortcuts, or
directly at `/system` and `/usage`. Non-administrators cannot open either page,
and the admin tabs are closed if a signed-in account loses the `admin` role.

## System Diagnostics

The System page (`/system`) reports:

- **Host**: hostname, platform, kernel release, architecture, uptime, logical
  CPU count, CPU model, load average, and whether the process looks
  containerized. There is no CPU utilization percentage; CPU load is the load
  average only.
- **Runtime**: application version, Node.js version, process id, process
  uptime, and working directory.
- **Memory**: total, free, and used host memory, plus the process RSS and heap
  figures.
- **Filesystems**: capacity and usage for the runtime filesystem (`/`) and the
  data directory (`DATA_DIR`).
- **Network**: interface names and addresses, with received/transmitted byte
  counters on Linux.
- **Docker**: engine version, host OS, kernel, CPU and memory as the engine
  reports them, and container counts plus a reduced container list, when the
  Docker socket is available.

The page refreshes every 30 seconds while its tab is focused and has a manual
refresh button. The backend endpoint is `GET /api/system`, guarded by
authentication, an active administrator role, and a per-user rate limit of 120
requests per 15 minutes. Responses are never cached (`Cache-Control:
no-store`), and every request collects fresh values.

### Docker socket dependency

The Docker section works only through a local Unix socket. Libre WebUI uses
`WORK_DOCKER_SOCKET` when set, otherwise `DOCKER_HOST` when it is a `unix://`
URL, otherwise `/var/run/docker.sock`. A remote TCP Docker endpoint is
deliberately not queried. The requests are strictly read-only engine `GET`s
(version, info, container list) with a 4-second timeout and a bounded response
size, and the container list is capped at 100 entries.

Without a usable socket the rest of the page still works: the Docker panel
reports why it is unavailable — socket not mounted, mounted but unreadable,
daemon unreachable, or remote endpoint — instead of failing the whole request.

### What the page reveals, and to whom

The container list is reduced on purpose: short id, name, image, state, and
created time. Environment variables, labels, mounts, container commands, and
inspect payloads are never included, and no credentials appear anywhere in the
response.

The page still shows real infrastructure detail — hostname, working directory,
internal IP addresses, and the names and images of every container on the
Docker host, not only Libre WebUI's own. That is consistent with the trust
model: in a Docker deployment every Libre WebUI administrator is already
effectively a host administrator (see [Docker](./DOCKER)). Grant the `admin`
role accordingly.

## Usage Analytics

The Usage page (`/usage`) charts outbound provider traffic. Metering happens on
the server at the plugin provider boundary, so it covers every plugin-backed
call regardless of which surface made it: chat (streaming and non-streaming),
embeddings, image generation, TTS, audio and video generation, and plugin-backed
Work runs. Requests to local Ollama are not metered.

Each event records:

- plugin id and a snapshot of the plugin name
- capability (`chat`, `embedding`, `image`, `stt`, `tts`, `audio`, `video`)
- model
- status: `success`, `error`, or `cancelled` (an aborted stream counts as
  cancelled)
- token counts, only when the provider returned usage metadata
- unit counters appropriate to the capability (characters for TTS, images,
  embedding inputs, jobs for video, bytes for audio)
- end-to-end duration and a timestamp
- the requesting user id

Nothing else is stored. **Prompts, responses, provider endpoints, credentials,
and provider error bodies are never written to the usage table** — a failed
call is recorded only as `status = 'error'`. The events live in the instance's
own SQLite database and are kept for **400 days**; older rows are pruned
opportunistically on write, at most once per day. Metering is best-effort by
design and can never make a provider request fail.

The page offers 7, 30, and 90-day ranges over a single admin-only endpoint,
`GET /api/plugins/usage?days=<1..365>` (default 30). It shows total calls,
reported tokens, success rate, and average latency, a daily chart switchable
between calls and tokens, a per-model table, per-plugin traffic shares, and the
capability mix. Token totals include only calls where the provider reported
usage metadata.

There is no switch to disable metering. Because the data is aggregated across
accounts, inspecting it is restricted to administrators.

### OpenRouter attribution

Since 0.18.0, requests to OpenRouter identify the application through
OpenRouter's app-attribution headers (`HTTP-Referer: https://librewebui.org`,
an application title, and category hints). These headers are sent only when the
request goes to `https://openrouter.ai` itself — never to a custom or
self-hosted route — and they add nothing to what is stored locally.

## Related Docs

- [Authentication](./AUTHENTICATION)
- [Docker](./DOCKER)
- [Plugin Architecture](./PLUGIN_ARCHITECTURE)
- [Environment Variables](./ENVIRONMENT_VARIABLES)

---

# Video & Audio Generation

URL: https://docs.librewebui.org/media-generation

# Video & Audio Generation

Libre WebUI 0.18.0 extends generation beyond images: provider plugins can
declare video and audio capabilities, and everything generated — images,
videos, speech, and sound — lands in one per-user media gallery.

Media generation is available to every authenticated user. The gallery is
strictly per-user: every read, content fetch, and delete is scoped to the
signed-in account.

## Plugin Capability Blocks

A plugin definition declares each media capability as its own block:

```json
"capabilities": {
  "image": { "endpoint": "...", "model_map": ["..."], "config": { ... } },
  "tts":   { "endpoint": "...", "model_map": ["..."], "config": { ... } },
  "audio": { "endpoint": "...", "model_map": ["..."], "config": { ... } },
  "video": { "endpoint": "...", "model_map": ["..."], "config": { ... } }
}
```

Every block has an `endpoint`, a `model_map` fallback list, an optional
`models_endpoint` for live model discovery, and a `config` object with
capability-specific options — sizes and aspect ratios for images, voices and
formats for speech, resolutions, aspect ratios, and durations for video. A
video provider may also declare a prompt-ID `cancel_endpoint` and
`cancel_method`; Libre does not infer cancellation support from an ordinary
generation endpoint.

Two audio capabilities exist and both end up in the gallery as audio:

- `tts` is speech: text is read aloud in a selected voice.
- `audio` is sound: a model generates audio content from a prompt.

OpenRouter (`plugins/openrouter.json`) is currently the only bundled plugin
that declares `video` and `audio` blocks. When a `models_endpoint` is present,
the model list refreshes on the normal discovery cycle (see
[Environment Variables](./ENVIRONMENT_VARIABLES) for the discovery TTL
settings); the `model_map` remains the fallback.

## Generating Media

Open **Imagine** (`/gallery`). The header offers **Generate** for images (when
image generation is enabled in Settings), plus **Video** and **Audio** panels.

Speech and sound generation are synchronous: the request runs, the result is
saved to the gallery, and the response returns the finished item. **Cancel**
aborts the browser request and Libre's outbound provider request; a cancelled
result is not saved. Image generation follows the same disconnect-cancellation
contract.

For an accepted ComfyUI workflow, Libre sends both the prompt-ID job-cancel
operation and a prompt-ID queue deletion, then waits up to three seconds for
that teardown before releasing the request. It never calls ComfyUI's unscoped
interrupt operation, which could stop another user's workflow. Current ComfyUI
releases expose `/api/jobs/:promptId/cancel` for a running workflow. On an old
release without that operation, Libre can still remove the exact pending queue
item, but cannot safely stop an already-running workflow; upgrade ComfyUI for
the complete cancellation contract.

TTS plugins can also declare voice cloning. For those models, the Audio panel
shows a reference-audio upload and, when the provider requires it, an exact
transcript field. Libre WebUI validates the manifest's file type and size
limits, holds the upload in memory, and forwards it only to the selected
provider. Only the generated speech is placed in the gallery.

A clone can optionally be saved as a reusable, named voice for the same plugin
and model. Saving requires a separate storage-consent confirmation. Libre WebUI
encrypts the original reference and transcript in a user-owned voice profile;
it does not use generated speech as the reference. Saved profiles can be
selected or permanently deleted under **Settings → Text-to-Speech**. The
configured provider receives the stored reference again whenever it generates
a Speech batch. The profile is bound to that provider's approved routing; if
the plugin definition or endpoint changes, recreate the profile to consent to
the new destination. Only use recordings from speakers who consented to both
the cloning request and any requested storage.

Voice profiles are intentionally omitted from Libre WebUI's general data
export because they contain biometric source material. Back up the encrypted
application database and `ENCRYPTION_KEY` together if you need disaster
recovery; otherwise recreate profiles from the original consented recordings.

### Video Job Lifecycle

Video generation is asynchronous. Submitting a job
(`POST /api/media/video/generate`) returns `202` with a job record, and the
job moves through `pending`, `in_progress`, and finally `completed` or
`failed`.

- Submission is detached from the browser response after validation. Libre
  persists the provider job ID immediately after acceptance even if the panel
  or network connection closes while the provider is replying.
- `GET /api/media/video/jobs` lists only the authenticated user's saved handles;
  the panel requests up to 100 active handles whenever it opens. A pending job
  can therefore be reopened after navigation, refresh, or disconnect.
- A durable `media.video.resume.v1` job polls the provider and downloads a
  completed result even when the panel is closed. Solo mode runs that handler
  in the embedded worker; team mode runs it in the external worker. Leases,
  bounded retry, actor revalidation, and conditional completion let another
  worker reclaim the job after a process dies without creating a duplicate
  gallery row or blob reference. The existing resume/GET endpoints remain
  compatibility and status boundaries; the UI may still poll them for display.
- Closing the panel or choosing **Stop waiting** aborts only the current status
  or download transport. A provider-side **Cancel job** action appears only
  when that plugin explicitly declares a job-ID cancellation endpoint. On a
  confirmed provider cancellation, Libre removes the saved local handle.
- On completion the backend downloads the video (200 MB cap, HTTP redirects
  not followed) and saves it to the gallery.
- The job record stores the plugin, model, options, status, and the prompt
  (encrypted at rest). Completed and failed job records older than 30 days are
  pruned opportunistically; pending handles are not expired by that cleanup.

## The Unified Gallery

The gallery lists all media kinds interleaved by creation time, with filter
pills for **All**, **Images**, **Videos**, and **Audio**. Videos and audio play
inline; images open in the lightbox; every item can be downloaded or deleted.

Storage and serving are deliberately conservative:

- Media is stored encrypted inside the application database (under
  `DATA_DIR`), not as loose files on disk. Back up the database and
  `ENCRYPTION_KEY` together, as with all encrypted data.
- API responses never embed media payloads; items reference a per-item content
  URL instead.
- Served content must match a per-kind MIME allowlist and the stored type, is
  capped at 200 MB, and is delivered with `X-Content-Type-Options: nosniff`
  and a `Content-Security-Policy` that sandboxes the response.

The previous image-only endpoints and the image generation panel keep working
unchanged; they write into the same gallery.

## Rate Limits

The media API is rate-limited per client:

| Operation                             | Limit                   |
| ------------------------------------- | ----------------------- |
| Generation (video, speech, sound)     | 10 requests per minute  |
| Video job polling                     | 60 requests per minute  |
| Gallery listing, content, and deletes | 120 requests per minute |

The 30-second UI poll stays well inside the polling budget.

## Metering and Privacy

Media generation calls are metered in the administrator
[usage analytics](./SYSTEM_MONITORING) like every other outbound provider
call: plugin, model, status, duration, and unit counts. Prompts and generated
content are never written to the usage records. The generated media itself and
the video job's prompt exist only in the user's own encrypted rows.

As with chat, the configured provider receives the prompt and returns the
content — provider pricing, retention, and content policies apply.

## Related Docs

- [Plugin Architecture](./PLUGIN_ARCHITECTURE)
- [Connect Third-Party and Self-Hosted Providers](./PROVIDER_CONNECTIONS)
- [System Diagnostics & Usage Analytics](./SYSTEM_MONITORING)
- [Environment Variables](./ENVIRONMENT_VARIABLES)
- [Database Encryption](./DATABASE_ENCRYPTION)

---

# Web Search

URL: https://docs.librewebui.org/web-search

# Web Search

Libre WebUI can search the web and hand the results to your models — without
sending anything to a commercial search API. Search runs through a
[SearXNG](https://docs.searxng.org/) instance you host yourself, next to the
app. Queries leave your server only as anonymous SearXNG requests to the
public search engines it aggregates.

Like every dual-use capability in Libre WebUI, it ships **off**. An
administrator turns it on once; until then no search UI exists anywhere.

## How it works

There are three pieces, and each one is invisible until the previous one
exists:

1. **A SearXNG instance** the backend can reach. The bundled private deploy
   stack includes one; any instance with the JSON API enabled works.
2. **The admin setting** — Settings > Connections > **Search** (the tab is
   admin-only). Set the SearXNG URL, flip **Enable web search**, and use
   **Test connection** to prove the wiring with a live query.
3. **Who may use it** — the **Web search** card in User Management, next to
   the Work access and model download controls. Off (the default) keeps
   search admins-only even while it is enabled; on opens it to all active
   users. The backend enforces this on every request.
4. **The per-use controls** that appear for permitted users:
   - **Chat:** a globe toggle in the composer. When it is on, the backend
     searches for your message before generating, injects the top results as
     context, and the reply shows numbered **source chips** linking to each
     result. This works with _every_ model — including small local models
     with no tool-calling support — because the results arrive as context,
     not as a tool the model must know how to call.
   - **Work:** tasks whose network access is on gain a `web_search` tool in
     the agent loop. The model decides when to call it, like any other tool.
     Offline tasks (network disabled) never see the tool, even though the
     search request itself would egress from the backend, not the sandbox.

A failed search never fails the turn: the model answers without the context
and the run continues.

## Setup with the bundled stack

`deploy/private/docker-compose.yml` already contains the `searxng` service:
internal-only (never published to the host), hardened, and pre-wired into
the app through `SEARXNG_URL=http://searxng:8080`.

1. Add a secret to `.env`:

   ```env
   SEARXNG_SECRET=any-long-random-value
   ```

2. `docker compose up -d`.

3. As an administrator: Settings > Connections > **Search** — the URL is
   pre-filled from the environment — enable, then **Test connection**.

## Setup with your own SearXNG

Any reachable SearXNG instance works, with one requirement: the **JSON
format** must be enabled in its `settings.yml` (most public instances
disable it):

```yaml
search:
  formats:
    - html
    - json
```

Then paste its base URL (for example `http://127.0.0.1:8888` or
`https://search.example.com`) into Settings > Connections > Search.

## Environment variables

| Variable      | Default | Purpose                                                                     |
| ------------- | ------- | --------------------------------------------------------------------------- |
| `SEARXNG_URL` | unset   | Pre-fills the URL in the Search settings; enabling is still an admin action |

The setting itself (enabled + URL) is persisted in the database, so it
survives restarts and takes effect immediately without redeploying.

## Privacy and scope

- Searches run **server-side**. Browsers never contact the search engine,
  and no API key or account is involved anywhere.
- Result text is bounded before it reaches model context (500 characters
  per result, at most 10 results), and only `http(s)` result URLs are kept.
- The bundled instance is reachable only on the stack's internal network.
  Its rate limiter is off for that reason; do not publish it.

## Troubleshooting

**No globe in the composer / no `web_search` tool in Work.** Search is not
enabled (Settings > Connections > Search), or the account is not permitted:
regular users need the **Web search** toggle in User Management turned on.

**Test connection fails with an HTTP 403.** The instance does not allow the
JSON format. Add `json` to `search.formats` in its `settings.yml` (see
above) and restart it.

**Test connection cannot reach the service.** The URL must be reachable
_from the backend_, not from your browser. Inside the bundled stack that is
`http://searxng:8080`; `localhost` inside a container is the container
itself.

**Replies ignore the results.** The model still decides what to use. Small
models follow the injected context better when the question is concrete;
the sources under the reply always show what was retrieved.

---

# LongCat AudioDiT Integration

URL: https://docs.librewebui.org/longcat-audiodit

# LongCat AudioDiT Integration

Libre WebUI bundles a JSON plugin and a local HTTP adapter for the official
`meituan-longcat/LongCat-AudioDiT-1B` and
`meituan-longcat/LongCat-AudioDiT-3.5B` checkpoints. The upstream project
provides a Python API rather than an HTTP server, so the adapter in
`examples/longcat-audiodit-server` supplies model discovery, JSON speech, and
multipart voice-cloning endpoints.

AudioDiT returns complete 24 kHz mono WAV files rather than a streaming audio
response. Libre WebUI therefore splits longer replies at sentence and phrase
boundaries, generates a bounded set of batches ahead, and schedules decoded
audio in order for continuous playback.

## Requirements

An NVIDIA CUDA GPU is the practical target. Start with the 1B checkpoint; the
3.5B checkpoint needs substantially more VRAM and takes longer to load. CPU is
available for experimentation but is unlikely to be interactive.

## Start the adapter

Clone the official implementation and create an isolated environment:

```bash
git clone https://github.com/meituan-longcat/LongCat-AudioDiT.git
python3 -m venv .venv
source .venv/bin/activate
pip install -r examples/longcat-audiodit-server/requirements.txt
export PYTHONPATH="$PWD/LongCat-AudioDiT"
```

Then start one checkpoint:

```bash
python examples/longcat-audiodit-server/server.py \
  --model meituan-longcat/LongCat-AudioDiT-1B \
  --device cuda:0
```

The server binds to `127.0.0.1:8300` by default. It is intentionally
authentication-free for local use, so do not expose it directly to an
untrusted network. Use an authenticated HTTPS gateway when Libre WebUI and the
adapter are on different hosts: reusable voices send the decrypted reference
recording to that endpoint for every Speech batch. A CUDA Docker example and
health check are documented in `examples/longcat-audiodit-server/README.md`.

## Enable the plugin

Open **Settings → Plugins → LongCat AudioDiT**, activate it, and leave the
default local endpoints unless the adapter runs elsewhere. Model discovery
advertises only the checkpoint resident in that server process. Restart the
adapter to switch between the 1B and 3.5B models.

When a gateway such as llama-swap fronts the adapter, configure the Models API
Endpoint as the adapter's `GET /v1/models` route through that gateway. Do not
point model discovery at `POST /v1/audio/speech`: failed discovery falls back
to both manifest checkpoints and can let the UI select a checkpoint that the
adapter did not load.

Use **Settings → Text-to-Speech** to select LongCat for chat playback. Natural
batched playback is enabled by default. The shared 140-character provider cap
keeps dense Chinese text inside the 1B checkpoint's shorter duration window;
Libre WebUI automatically creates multiple batches for longer responses.

## Voice cloning

Open **Imagine → Audio**, select a LongCat speech model, and enable **Clone a
reference voice**. Upload a clean recording and enter the exact words spoken
in it. A 3–10 second, single-speaker clip with little background noise works
best; the adapter rejects clips above 15 seconds or 10 MiB.

Cloning can remain a one-time generation, or you can select **Save as a reusable
voice**, give the voice a private name, and explicitly confirm that the speaker
consented to storage. Saved voices become selectable for the same LongCat model
under **Settings → Text-to-Speech**. Chat read-aloud and autoplay then reuse
that voice across Libre WebUI's sentence-aware batches.

The reference consumes part of AudioDiT's duration window. If the requested
speech would not fit in the remaining time, the adapter returns a clear client
error instead of silently clipping the audio; shorten the reference or the
generated passage and try again.

Only clone a voice with the speaker's explicit permission. For a one-time
generation, Libre WebUI keeps the reference in memory and the adapter removes
its temporary decoding file after the request. When you explicitly save a
reusable voice, Libre WebUI stores the original reference audio and exact
transcript in a user-scoped, AES-GCM-encrypted profile. It never substitutes
the generated imitation as the canonical reference. AudioDiT does not expose a
portable speaker embedding, so the original pair is decrypted and sent to the
configured provider for each generated batch. You can permanently delete the
profile from Text-to-Speech settings. A saved profile will fail closed if the
plugin's approved routing or endpoint changes; recreate it after verifying the
new destination.

Generated speech is stored separately in the user's encrypted media gallery.
Deleting a gallery result does not delete its saved voice profile, and deleting
a voice profile does not remove previously generated gallery audio.

The provider supports WAV, FLAC, MP3, and Ogg references. English and Mandarin
Chinese are the strongest documented language targets. LongCat does not
publish preset named voices, so ordinary synthesis uses the model default.

## Inference controls

The plugin exposes bounded advanced variables for ODE steps, guidance
strength, CFG/APG guidance method, and seed. Libre WebUI forwards only those
manifest-declared controls to both the speech and cloning endpoints. The
defaults match the upstream inference example and are a good starting point.

AudioDiT does not expose playback-speed control. For compatibility with the
OpenAI speech request shape, the adapter accepts `speed` values from `0.25`
through `4.0` and intentionally ignores them. The model determines the actual
speech timing; choosing another speed does not time-stretch the generated WAV.

See the example's README for direct `curl` requests, Docker commands, exact
limits, and offline validation commands.

---

# Data Portability

URL: https://docs.librewebui.org/data-portability

# Data portability

Libre WebUI can export and import a versioned, per-user JSON archive from
**Settings → Data Management**. The archive is intended for moving supported
personal data between Libre WebUI installations or restoring that data into an
account. It is not a complete server backup.

## Archive version 3

The current format is identified by:

```json
{
  "format": "libre-webui-user-data",
  "version": 3,
  "integrity": {
    "algorithm": "sha256",
    "canonicalization": "libre-json-sort-v1",
    "digest": "<64 lowercase hexadecimal characters>"
  }
}
```

The backend creates the export from authenticated, user-scoped database
queries. It contains:

- user preferences, except a selected reusable voice-profile reference;
- chat folders;
- chat sessions, messages, branches, ratings, artifacts, and per-chat
  settings;
- standalone Notes;
- knowledge collections;
- extracted document content and metadata, session/collection associations,
  and text chunks.

Document embeddings are not exported because they are derived data. Regenerate
embeddings after import when semantic retrieval is enabled. Libre WebUI stores
the extracted text used by RAG, not the original uploaded PDF or text-file
bytes, so an archive cannot recreate the original upload byte-for-byte.

Each archive includes an `exclusions` list. Version 3 deliberately excludes:

- accounts, passwords, login sessions, and OAuth state;
- provider credentials and encrypted plugin variables;
- cloned-voice reference recordings and transcripts, which are biometric data
  and require separate consent-aware handling;
- personas and persona memory;
- generated image, audio, and video library files;
- Work tasks, runs, sandboxes, and Docker or Kubernetes volumes.

Use a database/data-directory backup with the same `ENCRYPTION_KEY` for full
server recovery. Work also requires a consistent backup of its named volumes.
See [SQLite migration and backup](./10-SQLITE_MIGRATION.md) and
[Work workspaces](./33-WORKSPACES.md#deletion-account-changes-and-backup).

## Integrity and export validation

Version 3 protects the archive payload with a SHA-256 integrity digest. The
`libre-json-sort-v1` canonical form omits the top-level `integrity` field,
sorts every JSON object's keys lexicographically, preserves array order, and
hashes the resulting compact JSON as UTF-8. Import rejects a version 3 archive
whose digest does not match, even if its JSON remains syntactically valid.

This digest detects accidental corruption and post-export changes. It is not a
digital signature, does not authenticate who created the file, and does not
make the archive confidential. Treat an archive like any other copy of the
user's private chats and Notes.

Before offering a download, export runs the same schema, field-size, ID, and
archive-count checks used by import. It also verifies that the pretty-printed
JSON downloaded by the web UI is no larger than the 50 MiB upload limit. Export
returns a precise validation error instead of offering a file that Libre WebUI
already knows it cannot restore.

Current archive and account limits are:

- 50 MiB per uploaded or generated archive;
- 100 chat folders;
- 5,000 chat sessions;
- 100,000 chat messages;
- 100 Notes, with titles up to 200 characters and content up to 200,000
  characters;
- 5,000 knowledge collections;
- 5,000 documents;
- 100,000 document chunks;
- individual general content fields up to 2,000,000 characters and IDs up to
  256 characters, with narrower bounds where the runtime resource does so.

## Safe import behavior

Selecting a file asks the backend to preflight it immediately. Settings shows
the incoming totals, projected create/overwrite/skip counts, ID remaps, and
migration warnings before enabling the final Import action. Changing the
conflict policy calculates and displays a fresh preview.

Preflight verifies the integrity digest where available, migrates supported old
formats, validates the complete schema, resource counts, unique IDs,
timestamps, content bounds, and included relationships, and plans conflicts and
reference remapping without writing data. Dangling folder, collection,
message-parent, or document associations are rejected instead of being
silently dropped. The backend repeats validation and conflict planning for the
actual import. All writes occur in one database transaction on both supported
SQLite and PostgreSQL backends; an error rolls back preferences, folders,
sessions/messages, Notes, collections, documents, and chunks together.

Two conflict policies are available:

- **Skip duplicates** keeps records with matching IDs and imports new records.
  Preferences are merged with the account's current preferences.
- **Overwrite existing** replaces records with matching IDs. Preferences are
  replaced over Libre WebUI defaults. Records absent from the archive are never
  deleted.

Both policies are idempotent for records with matching IDs. If an ID is already
owned by another account on the target server, Libre WebUI deterministically
remaps it and every included reference to it. It never overwrites or reads
another user's resource. References to excluded or unavailable resources, such
as a persona from another installation, remain a documented exception: the
preflight reports that the session will be detached before import.

The result shown in Settings reports created, overwritten, and skipped counts
for folders, sessions, Notes, collections, and documents. After a successful
import, Libre reloads preferences, chats, and folders and refreshes documents.

## Older archives

The importer accepts version 2 `libre-webui-user-data` archives and migrates
them to version 3 during validation. Version 2 did not have an integrity digest
and did not contain Notes, so Libre cannot verify its origin or recover Notes
that were never exported. The preflight preview states both limitations.

The importer also accepts the former `libre-webui-export` version `1.0` shape.
That browser-generated format contained preferences and only the sessions
loaded in that browser. Its `documents` array was always empty, and it did not
contain folders, Notes, knowledge collections, or document chunks. Libre
reports these migration limitations before import.

## HTTP endpoints

All endpoints require the authenticated user's bearer token or session:

| Method | Endpoint                            | Purpose                             |
| ------ | ----------------------------------- | ----------------------------------- |
| `GET`  | `/api/preferences/export`           | Build the current user's v3 archive |
| `POST` | `/api/preferences/import/preflight` | Validate and plan without writes    |
| `POST` | `/api/preferences/import`           | Validate and import transactionally |

The web UI sends the archive as a `multipart/form-data` field named `archive`
and the conflict policy as a `strategy` field. The upload limit is 50 MiB. For
smaller API-driven migrations, the two POST endpoints also accept JSON:

```json
{
  "data": { "format": "libre-webui-user-data", "version": 3 },
  "strategy": "skip"
}
```

`strategy` is either `skip` or `overwrite`. For compatibility with the former
preferences-only client, `mergeStrategy: "merge"` maps to `skip` and
`mergeStrategy: "replace"` maps to `overwrite`.

---

# Speech to Text

URL: https://docs.librewebui.org/speech-to-text

# Speech to Text

Libre WebUI can transcribe microphone recordings through an active plugin that
declares a speech-to-text capability. When the browser exposes its speech
recognition service, Chat selects that service by default; its implementation
and data handling are controlled by the browser vendor and are not necessarily
on-device. Select a named provider explicitly to use provider-backed STT. When
the browser service is unavailable, Chat selects the first compatible provider.

Bundled support includes:

- OpenAI transcription models through a multipart
  `/v1/audio/transcriptions` request; and
- Hugging Face automatic speech recognition models through its raw-audio
  inference endpoint.

The browser records only after the user presses the microphone button. Chat
shows the selected provider and transfer notice before recording. Press the
button again to stop recording; while transcription is running, the same button
cancels the provider request. Libre inserts the returned transcript into the
composer. The recording is held in memory for the request and is not saved by
Libre WebUI. Navigating to another chat cancels pending microphone permission,
recording, and transcription work.

## Provider contract

An STT capability declares an endpoint, model map, accepted formats, request
mode, and optional endpoint variable. Libre validates the authenticated user's
exact plugin/model route before forwarding audio. Libre currently accepts
uncompressed PCM WAV and Opus WebM recordings. It validates the filename
extension, declared MIME type, container structure, codec identifier, sample
rate, channels, decoded-size ratio, and duration before forwarding audio.
Requests are limited to 25 MiB and 300 seconds globally (or a lower manifest
limit), bounded to two concurrent transcriptions per user and six across the
deployment, and aborted when the browser disconnects or the renewable shared
admission permit is lost. Redis admission failure in team mode fails closed;
it never creates an independent per-replica allowance.

Provider credentials remain on the backend. Libre refuses redirects so an
authorization header or audio recording cannot be forwarded to a different
host. Provider authentication failures are surfaced as an upstream failure,
not as an expired Libre session.

Use HTTPS for the Libre WebUI origin and remote providers. Browsers do not expose
microphone capture to an insecure remote origin, and Libre does not advertise
either speech path unless the browser exposes its secure-origin media APIs. The
provider may retain or process recordings under its own terms, so review that
policy before dictating sensitive text.

---

# Capability Contracts

URL: https://docs.librewebui.org/capability-contracts

# Capability Contracts

<!-- Generated from scripts/capability-contracts.json. Do not edit this table by hand. -->

This inventory is enforced by `scripts/test-capability-contracts.mjs`. A
provider capability is not complete until its schema and catalog mapping,
executable handler, matching browser client, named UI action, documentation,
focused behavioral test, and bundled manifests agree.

| Capability        | Plugin types         | Executable route                                                                 | Browser client                                                                 | UI action                                                               | Documentation                    | Focused behavior tests                                                                                                                                                                                                                                                                      | Bundled definitions                                                                                                                          |
| ----------------- | -------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Chat / completion | `chat`, `completion` | `WS /ws (chat_stream)`                                                           | `frontend/src/hooks/useChat.ts`                                                | `frontend/src/pages/ChatPage.tsx` — `handleSendMessage`                 | `docs/08-PLUGIN_ARCHITECTURE.md` | `scripts/test-chat-provider-selection.mjs` (`test:package`) — exact plugin routing reaches the selected provider for regular and streaming requests                                                                                                                                         | `anthropic`, `codex-oauth`, `gemini`, `github`, `groq`, `huggingface`, `kimi-code`, `llama-cpp`, `mistral`, `mlx-lm`, `openai`, `openrouter` |
| Embeddings        | `embedding`          | `POST /api/ollama/embed`<br />`GET /api/embeddings/models` (discovery only)      | `frontend/src/utils/api/modelApi.ts`                                           | `frontend/src/components/ModelManager.tsx` — `handleGenerateEmbeddings` | `docs/09-RAG_FEATURE.md`         | `scripts/test-plugin-capability-routing.mjs` (`test:package`) — Hugging Face capabilities ignore the generic Chat endpoint and use task payloads                                                                                                                                            | `huggingface`                                                                                                                                |
| Image generation  | `image`              | `POST /api/image-gen/generate`                                                   | `frontend/src/utils/api/imageGenApi.ts`                                        | `frontend/src/components/ImageGenerationPanel.tsx` — `handleGenerate`   | `docs/38-MEDIA_GENERATION.md`    | `scripts/test-image-generation-routing.mjs` (`test:package`) — image generation uses the selected provider and user-scoped OpenAI settings                                                                                                                                                  | `comfyui`, `huggingface`, `openai`, `openrouter`                                                                                             |
| Speech to text    | `stt`                | `POST /api/stt/transcribe`                                                       | `frontend/src/utils/api/sttApi.ts`                                             | `frontend/src/components/ChatInput.tsx` — `toggleDictation`             | `docs/42-SPEECH_TO_TEXT.md`      | `scripts/test-stt-routing.mjs` (`test:package`) — STT sends an OpenAI-compatible multipart request to the selected route<br />`frontend/e2e/stt.spec.ts` (`test:e2e`) — provider speech input discloses its route and transcribes recorded audio                                            | `huggingface`, `openai`                                                                                                                      |
| Text to speech    | `tts`                | `POST /api/tts/generate`                                                         | `frontend/src/utils/api/ttsApi.ts`                                             | `frontend/src/components/TTSButton.tsx` — `handlePlay`                  | `docs/38-MEDIA_GENERATION.md`    | `scripts/test-tts-routing.mjs` (`test:package`) — TTS routes a shared model alias through the selected plugin and user valve<br />`frontend/e2e/tts-playback.spec.ts` (`test:e2e`) — batched read-aloud reuses the selected saved voice for every batch                                     | `elevenlabs`, `huggingface`, `kyutai-tts-1.6b`, `kyutai-tts`, `longcat-audiodit`, `openai-tts`, `openai`, `openrouter`, `qwen-tts`           |
| Audio generation  | `audio`              | `POST /api/media/sound/generate`                                                 | `frontend/src/utils/api/mediaApi.ts`                                           | `frontend/src/components/MediaGenerationPanel.tsx` — `handleGenerate`   | `docs/38-MEDIA_GENERATION.md`    | `scripts/test-openrouter-media-routing.mjs` (`test:package`) — OpenRouter audio-output models stream generated sound bytes                                                                                                                                                                  | `openrouter`                                                                                                                                 |
| Video generation  | `video`              | `POST /api/media/video/generate`<br />`POST /api/media/video/jobs/:jobId/resume` | `frontend/src/utils/api/mediaApi.ts`<br />`frontend/src/utils/api/mediaApi.ts` | `frontend/src/components/MediaGenerationPanel.tsx` — `handleGenerate`   | `docs/38-MEDIA_GENERATION.md`    | `scripts/test-openrouter-media-routing.mjs` (`test:package`) — video generation submits, polls, and downloads through the provider endpoint<br />`scripts/test-openrouter-media-routing.mjs` (`test:package`) — prepared video and resume publications resolve lost commit acknowledgements | `openrouter`                                                                                                                                 |

## Enforcement

The package gate rejects undeclared schema or plugin types, stale catalog
mappings, discovery-only routes presented as execution, handlers without a
matching browser transport, UI actions without an invocation and visible
trigger, documentation without capability-specific claims, tests without
behavior inside the named test case, stale generated inventory, and invalid
manifest endpoints, model maps, or defaults.

Embedding generation executes through `POST /api/ollama/embed` and can route
to a selected embedding plugin. `GET /api/embeddings/models` lists models;
the gate records it as discovery-only and never accepts it as proof that
embedding generation works.

The source contract identifies each focused behavior test and its runner.
Backend scripts must be registered in `test:package`; frontend specs must
be discoverable by the Playwright-based `test:e2e` script. Live discovery
may narrow a model catalog, but it does not create a new executable
capability.

---

# Recovery Readiness

URL: https://docs.librewebui.org/recovery-readiness

# Recovery Readiness

Libre WebUI provides a read-only recovery inventory as the first backup and
restore safety gate. It reports what known state exists and which detected
conditions block a snapshot. It does not acquire a maintenance lock, copy,
encrypt, upload, delete, repair, or restore data.

```bash
libre-webui recovery-check --json > recovery-inventory.json
```

From a source checkout, run `npm run build:backend` once and replace
`libre-webui recovery-check` with `npm run recovery:check --`. Packaged npx and
Homebrew installs inspect `~/.libre-webui` by default; `DATA_DIR` and explicit
path options override that location.

The command exits with status `0` when no blockers are found, `1` when the
report is complete but recovery blockers exist, and `2` for invalid arguments
or an unexpected collection failure. Use `--data-dir PATH` or `--database PATH`
to inspect a non-default location. A default or `--data-dir` volume inventory
accepts only the canonical `DATA_DIR/data.sqlite` file and rejects hard-linked,
symlinked, or non-regular database/WAL/SHM entries. An explicit `--database`
path may be outside `DATA_DIR`, but the selected database and any companions
must still be regular files and cannot be symlinks. When `--database` is used
without `--data-dir`, recovery treats the database's parent as its data root so
the matching key, blobs, and plugin definitions are inventoried together.

The runtime also reads historical plugin definitions from the deterministic
backend package `plugins` directory and, for a relative `PLUGINS_DIR`, its
historical backend-relative location. Recovery inventories those active legacy
paths and blocks a volume-only snapshot when they contain custom definitions.
Packaged deployments may pass `--legacy-plugins-dir PATH` more than once when
their image layout relocates those compatibility directories.

For the private Compose deployment, run it inside the deployed container so
the report describes that container's mounted volume, code, and secrets:

```bash
docker exec libre-webui \
  libre-webui recovery-check --json --data-dir /app/backend/data
```

## What the inventory checks

The versioned JSON report records:

- application, Node.js, operating-system, and architecture versions;
- SQLite file and WAL/SHM sizes, `quick_check`, foreign-key validation, schema
  fingerprint, user version, missing required tables, and no-follow source-file
  validation before a private inspection snapshot is created;
- data-directory readability, writability, file count, and byte count;
- the selected encryption-key source and a one-way 16-character fingerprint;
- no-follow, single-link validation for the persistent `.encryption_key` file;
- presence, counts, sizes, and data-directory inclusion for custom plugin
  definitions, plus the encrypted local blob root, embedded media, voice
  references, document text, legacy document vectors, and platform vectors
  with their ACL/filter rows;
- bounded, read-only authentication of every canonical local blob object and
  embedded platform-vector envelope, including full blob chunk/checksum
  verification and configured-key availability;
- bounded, read-only authentication of every recognizable legacy text
  AES-GCM envelope across chats, notes, documents, preferences, plugin
  secrets, gallery/media state, and account email, plus every AAD-bound saved
  voice name, recording, and transcript envelope;
- Work task/run/preview counts and the expected Docker volumes, Kubernetes
  PVCs, or hashed host-path identities; Docker volumes must carry both the
  managed label and the exact owning task ID;
- legacy media-generation job states plus durable jobs by state, attempts by
  outcome, event stream/event counts, and the last global event cursor;
- bounded, read-only authentication of every encrypted durable job and event
  payload, plus bounded syntax validation of every opaque reference payload;
  and
- explicit blockers, warnings, and data that lives outside the application
  data directory.

The report never includes encryption keys, JWT/session secrets, provider
credentials, plugin contents, user content, or literal host-workspace paths.
Only secret-presence booleans and the non-reversible encryption-key fingerprint
are emitted.

A read-only data mount is valid for recovery inspection and produces a warning,
not a blocker. Application readiness still requires writable storage; never
start Libre WebUI against the read-only snapshot used by the backup helper.

## Blockers

Treat any blocker as a failed recovery gate. Typical blockers include a
missing or corrupt database, an incomplete schema, an absent/conflicting key,
corrupt or unauthenticated legacy or platform ciphertext, exceeded
verification bounds,
an unreadable data directory, a linked or non-regular SQLite source, active
Work runs or previews, media jobs, or
durable jobs, a missing or incorrectly labelled Work workspace, durable event
head mismatches or sequence gaps, custom plugin definitions outside the data
directory, or a runtime control plane that cannot verify external workspaces.
Quiesce active work and resolve missing dependencies before taking the
snapshot; do not edit the report to hide a blocker.

Encrypted durable payloads are authenticated against their job/event identity
and validated as canonical bounded JSON. Opaque reference payloads are bounded
and syntax-checked only: the current substrate has no authoritative blob
reference repository with which recovery can prove target existence or access.
The report marks `referenceTargetsVerified` false and warns whenever such
references are present; it never exposes payload or reference values.

Legacy text fields predate a mandatory envelope marker, so genuine plaintext
rows from older schema generations remain readable and are not reported as
authenticated ciphertext. Canonical envelopes are always authenticated;
three-part values with an envelope-width IV or authentication tag fail closed
when malformed. Saved voice fields have an unambiguous binary envelope and are
always required to authenticate against their profile, owner, and field
identity. The JSON `encryption.legacyCiphertext` section reports authenticated
text/binary record and byte totals without exposing plaintext.

When schema v4's `users.email_lookup` column is present, recovery also
authenticates every non-null email and recomputes its domain-separated keyed
lookup token. A missing or mismatched token, or a token attached to a null
email, blocks the snapshot. Pre-v4 databases remain compatible because they do
not have this derived lookup column.

## Current backup boundary

The private-deployment helper stops the application when it was running and
uses that container's immutable image, mounted data volume, and environment to
create an integrated solo archive. The manifest is Ed25519-signed and the
complete payload is encrypted with an operator-held AES-256-GCM backup key. It
contains SQLite, local blobs and embedded vectors, runtime selectors, and the
protected configuration needed to decrypt restored state. The helper verifies
the signature, ciphertext checksum, and decrypted payload before publishing
the archive and metadata report. `libre-webui-restore` accepts only a new
Docker volume, verifies the decrypted recovery inventory before copying any
data, and publishes recovered configuration as private files in a new target
directory.

Protected runtime configuration includes the PostgreSQL pool, connection,
idle, statement, and migration-lock timeouts; the Redis connection timeout;
both durable blob-quota settings; the platform selectors; and the S3 prefix and
addressing mode. These values are inside the signed and encrypted payload, not
the plaintext manifest, and are republished as mode-`0600` configuration on an
applied restore.

The solo archive does not include Docker Work volumes, Kubernetes PVCs,
host-bound workspace folders, Ollama models, or external provider state. Keep
those signed-manifest exclusions visible and snapshot external Work storage
separately. The team profile uses the separate offline team workflow: a
PostgreSQL exported snapshot, exact versioned S3 ciphertext objects, PGVector
inventory, runtime configuration, and key identity are sealed into the same
signed/encrypted archive format and verified against a clean PostgreSQL/S3
target during restore. Redis cache, presence, wake-ups, and leases are rebuilt
from canonical SQL state.

Team backup also authenticates every bounded encrypted durable job and event
payload inside the exact exported PostgreSQL snapshot. Its protected signed
inventory records the job, event, stream, cursor, envelope, reference, and
authenticated-plaintext totals. Every event stream must contain exactly the
contiguous sequence `1..last_sequence`, and PostgreSQL's global cursor sequence
must not lag the greatest stored cursor. Restore repeats these checks against
the clean target and requires the complete result to match the signed source
inventory before it reports success. Gaps between distinct global cursor values
are valid because PostgreSQL identity allocation is not transactional;
per-stream sequences are the contiguous ordering contract.

When `PLUGINS_DIR` points outside `DATA_DIR`, recovery inventories that exact
directory and marks it as excluded from the application-volume archive. Any
definitions there block the volume-only snapshot until the operator arranges a
matching plugin-directory snapshot. The same rule applies to active legacy
plugin directories. Symlinked, non-regular, or unreadable JSON definitions are
always blockers and are never followed or silently omitted.

Durable jobs and ordered events are active in both profiles. Recovery blocks
while a job attempt or Work execution is active, validates job/event payloads
and contiguous stream heads, and preserves their canonical SQL state. Solo
runs the bounded embedded worker; team runs the same registered handlers in an
external worker and uses Redis only for wake-up and fan-out.

For production, store encryption and JWT secrets in a protected secret
manager, keep backup archives off-host and encrypted, and test restores into a
clean compatible environment. The inventory is a preflight snapshot of known
state, not a maintenance lock or independent proof that every external resource
can be restored.

## Signed and encrypted backup commands

The examples below use the installed `libre-webui` command from global npm or
Homebrew. Without installing globally, replace `libre-webui` with
`npx --yes libre-webui@latest`. From a source checkout, build the backend once
and replace `libre-webui backup` with `npm run recovery:backup --`.
The production Docker image exposes the same command at
`/usr/local/bin/libre-webui`. Team backup and restore additionally require
PostgreSQL 16 `pg_dump` and `pg_restore`; they are included in the production
image and on the Homebrew formula's command path. Install a compatible
PostgreSQL client explicitly before using these commands from plain npm/npx.

Generate the
operator-held AES-256-GCM archive key and Ed25519 signing keypair in a private
directory, then move the private keys to protected off-host storage:

```bash
install -d -m 0700 /absolute/private/libre-backup-keys
libre-webui backup keygen \
  --directory /absolute/private/libre-backup-keys
```

For a quiesced solo data directory, create and independently verify an archive:

```bash
libre-webui backup create \
  --offline \
  --data-dir /absolute/path/to/libre-data \
  --output /absolute/backups/libre-solo.lwbackup \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-private-key /absolute/private/libre-backup-keys/backup-signing-private.pem

libre-webui backup verify \
  --archive /absolute/backups/libre-solo.lwbackup \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-public-key /absolute/private/libre-backup-keys/backup-signing-public.pem
```

Restore first as a preflight, then apply only to a new, empty target directory:

```bash
libre-webui backup restore-preflight \
  --archive /absolute/backups/libre-solo.lwbackup \
  --target /absolute/restore/libre-data \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-public-key /absolute/private/libre-backup-keys/backup-signing-public.pem

libre-webui backup restore-apply \
  --archive /absolute/backups/libre-solo.lwbackup \
  --target /absolute/restore/libre-data \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-public-key /absolute/private/libre-backup-keys/backup-signing-public.pem

libre-webui backup restore-verify \
  --target /absolute/restore/libre-data
```

For team mode, stop all application replicas and workers, keep the source
PostgreSQL/S3/keyring environment loaded, and create the coordinated archive:

```bash
libre-webui backup create-team \
  --offline \
  --output /absolute/backups/libre-team.lwbackup \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-private-key /absolute/private/libre-backup-keys/backup-signing-private.pem
```

Load environment variables for a distinct, empty PostgreSQL database and an
empty versioned S3 bucket before restore. Preflight verifies the signature and
encrypted archive, validates the protected inventory, and proves the selected
target database and bucket prefix are empty without publishing data. Apply
restores into those clean targets, verifies the resulting PostgreSQL schema,
exact S3 objects, and PGVector records, and writes the protected runtime
configuration into a new private directory:

```bash
libre-webui backup restore-team-preflight \
  --archive /absolute/backups/libre-team.lwbackup \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-public-key /absolute/private/libre-backup-keys/backup-signing-public.pem

libre-webui backup restore-team-apply \
  --archive /absolute/backups/libre-team.lwbackup \
  --configuration-output /absolute/restore/libre-team-config \
  --encryption-key /absolute/private/libre-backup-keys/backup-encryption.key \
  --signing-public-key /absolute/private/libre-backup-keys/backup-signing-public.pem
```

Never point a restore at the source database, source bucket, an existing data
directory, or a configuration directory containing files. Keep the public
signing key with the restore runbook; possession of the archive and public key
alone cannot decrypt the payload.

If team restore reports that rollback was incomplete, treat both selected
targets as dirty and do not retry immediately. Inspect and clean the target
PostgreSQL database, then enumerate and remove every object version and delete
marker under the exact target S3 prefix. Run `restore-team-preflight` again;
apply is safe to retry only after that clean-target preflight succeeds.

---

# Platform Foundation

URL: https://docs.librewebui.org/platform-foundation

# Platform Foundation

Libre WebUI supports a local-first `solo` profile and a shared `team` profile.
Solo uses SQLite, encrypted local blobs, encrypted embedded vectors, local
coordination, and an embedded durable worker. Team uses PostgreSQL,
S3-compatible private blobs, PGVector, Redis coordination, and an external
durable worker. Startup rejects mixed profiles instead of silently splitting
state between local and shared backends.

## Current milestone

| Area         | Implemented foundation                                                                | Remaining caller work                                                         |
| ------------ | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Persistence  | SQLite and PostgreSQL repositories, immutable migrations, pooled transactions         | New domains must use repository boundaries                                    |
| Blobs        | Encrypted local and S3-compatible streaming stores, ranges, checksums, durable quotas | Move chat attachments, avatars, and other remaining inline binary fields      |
| Vectors      | Encrypted embedded vectors, PGVector ACLs, and deletion-safe document index rebuilds  | New embedding callers must preserve the same authority and lifecycle contract |
| Coordination | Local and Redis events, cache, leases, rate limits, invalidation, and health          | Keep Redis non-authoritative                                                  |
| Jobs/events  | SQLite/PostgreSQL queues, transactional events, workers, retries, cancellation, admin | Every new side effect needs an idempotency or outbox design                   |
| Operations   | Health gates, signed/encrypted backup archives, clean-target restore, verification    | Exercise restore and cross-replica acceptance for each deployment environment |

## Runtime profiles

`LIBRE_PLATFORM_MODE=solo` is the default. It selects SQLite, local blobs,
embedded vectors, local coordination, and the embedded durable worker. Redis
may be selected in solo mode, but doing so does not make SQLite or local files
safe to share across replicas.

`LIBRE_PLATFORM_MODE=team` requires all shared dependencies at once:

- `DATABASE_BACKEND=postgres` with `DATABASE_URL`;
- `BLOB_STORE_BACKEND=s3`;
- `VECTOR_STORE_BACKEND=pgvector`;
- `COORDINATION_BACKEND=redis` with `REDIS_URL`; and
- `JOB_WORKER_MODE=external`.

These selectors are a coherent set. Team startup fails when any shared
dependency is missing or when a local backend is mixed into the profile.

### Migrate an existing solo installation

Stop every Libre application and worker before migrating. The examples use the
installed `libre-webui` command from global npm or Homebrew. Without installing
globally, replace it with `npx --yes libre-webui@latest`. From a source checkout,
build once and replace `libre-webui migrate-postgres` with
`npm run migrate:postgres --`. Configure the target PostgreSQL, S3, and
versioned encryption-key environment exactly as the target team deployment,
then run the read-only analysis first:

```bash
libre-webui migrate-postgres \
  --source /absolute/path/to/data.sqlite \
  --plugins /absolute/path/to/plugins \
  --mode dry-run
```

Apply only to the empty target identified by that report. A failed run leaves a
checksummed import journal; resume that same source and target rather than
starting an unrelated import:

```bash
libre-webui migrate-postgres \
  --source /absolute/path/to/data.sqlite \
  --plugins /absolute/path/to/plugins \
  --mode apply

# Only after an interrupted apply of this exact source and target:
libre-webui migrate-postgres \
  --source /absolute/path/to/data.sqlite \
  --plugins /absolute/path/to/plugins \
  --mode apply --resume

libre-webui migrate-postgres \
  --source /absolute/path/to/data.sqlite \
  --plugins /absolute/path/to/plugins \
  --mode validate
```

The completion marker is withheld until relational rows, plugin definitions,
local encrypted blobs, embedded vectors, and legacy persona vectors have all
been transferred and authenticated in PostgreSQL/S3/PGVector. The command
never invents a source encryption key: `ENCRYPTION_KEY` must match the source
`.encryption_key`, and `STORAGE_ENCRYPTION_KEYS` must contain the configured
active key plus the matching `legacy` entry.

### Run the bundled team profile

Start from the shipped fail-closed template. Keep the completed environment
file outside the repository and restrict it to its operator:

```bash
cp deploy/team/.env.example /absolute/path/to/libre-team.env
chmod 600 /absolute/path/to/libre-team.env
```

Replace every `REPLACE_*` value before startup. Generate the PostgreSQL
password with a URL-safe alphabet (for example, `openssl rand -hex 32`) because
the same literal is both the server password and part of `DATABASE_URL`.
`ENCRYPTION_KEY` and every value inside `STORAGE_ENCRYPTION_KEYS` must be
exactly 64 hexadecimal characters. On a fresh installation the `legacy` entry
must equal `ENCRYPTION_KEY`; for SQLite migration both must equal the source
key. Keep a different active key for new blob writes and retain old keys until
the object inventory proves they are unused.

The same file may set `POSTGRES_MIGRATION_MODE`, `POSTGRES_POOL_MAX`, the
supported PostgreSQL timeouts, `REDIS_CONNECT_TIMEOUT_MS`, `OLLAMA_BASE_URL`,
`OLLAMA_TIMEOUT`, `OLLAMA_LONG_OPERATION_TIMEOUT`, and `OLLAMA_MAX_CONTEXT`;
the shared Compose environment sends each value identically to the application
and external worker. The provider timeouts accept 1,000-3,600,000 milliseconds,
maximum context accepts 128-2,097,152 tokens, and the long timeout cannot be
shorter than the standard timeout; malformed values fail both server
entrypoints before state is created. Node-local Agent CLI binaries and Codex
OAuth token files are not supported by external durable workers, so the team
profile pins both provider paths off and startup rejects attempts to enable
them. Then start the application replicas, external durable worker,
PostgreSQL/PGVector, Redis, versioned MinIO bucket, and gateway:

```bash
docker compose --env-file /absolute/path/to/libre-team.env \
  -f docker-compose.team.yml up --build --scale libre-webui=3 -d
docker compose --env-file /absolute/path/to/libre-team.env \
  -f docker-compose.team.yml ps
```

The base team profile deliberately mounts no Docker socket, so Docker-backed
Work is unavailable. Enable it only by including the shipped production
overlay in every lifecycle command:

```bash
docker compose --env-file /absolute/path/to/libre-team.env \
  -f docker-compose.team.yml -f docker-compose.team.work.yml \
  up --build --scale libre-webui=3 -d
docker compose --env-file /absolute/path/to/libre-team.env \
  -f docker-compose.team.yml -f docker-compose.team.work.yml ps
```

That overlay points both application and worker processes at one filtered
Docker socket proxy on an internal-only network. Neither process receives the
raw socket or socket-group membership, and host-folder Work workspaces stay
disabled. The proxy exposes only info, images, containers, exec, volumes,
networks, and the write methods those lifecycle calls require. This narrows the
API surface but does not make Docker a tenant boundary: container creation can
still bind-mount host paths. Use a dedicated VM or rootless/separate Work daemon
when host isolation matters.

Do not expose the Compose-owned PostgreSQL, Redis, or MinIO services directly.
For managed dependencies, use the Helm team profile and retain verified TLS;
the Compose file disables PostgreSQL TLS only on its private project network.
Readiness remains failed until an external worker is present.

## Persistence and migration boundary

Identity and authorization now use asynchronous repositories. The repository
transaction callback receives a unit of work bound to the same database
connection; using the global repository from inside that callback is rejected.
This is the transaction boundary used by the PostgreSQL connection pool while
preserving the current SQLite behavior.

The SQLite migration coordinator adopts existing installations only after
validating the required schema. It records a numbered migration name and
checksum, verifies them on every startup, rejects newer/unknown/mismatched
ledgers, and fails startup when migration or schema validation fails. Readiness
and recovery inventory consume the same canonical inspection contract.

Before importing stateful application services, startup copies the existing
SQLite database and active WAL/SHM files into a private scratch directory and
validates that copy. `PLATFORM_PREFLIGHT_TMP_DIR` must have enough free space
for the database plus its WAL. The supplied Docker and Helm deployments mount
dedicated disk-backed temp storage there; startup does not rely on the bounded
`/tmp` tmpfs. A missing legacy encryption key or a historical nested data
directory blocks startup before a replacement key, database, or plugin state
can be created.

Schema v4 adds a keyed equality token for encrypted identity emails. Recovery
requires every token to be present and match its authenticated email. Startup
permits a missing token with an authenticated email or a non-envelope legacy
value during the narrow crash window after v4 commits, so repository
initialization can finish the encryption/token backfill. Older releases
accepted arbitrary email strings and used blank values to clear the field;
adoption preserves nonblank values and normalizes blanks to `NULL`. Damaged
envelope-shaped values and any non-null mismatch still fail preflight.

Application services use asynchronous dialect repositories. Native SQLite is
restricted to SQLite adapters, migration/recovery inspection, and explicitly
injected health checks. Runtime storage is initialized from the selected
`Persistence`; PostgreSQL activation never falls through to the SQLite
singleton or historical cwd-dependent JSON files.

The common durable-job runtime also stays driver-neutral. Actor authorization
is read through the selected identity repository, while native job repository
construction is confined to one adapter-composition boundary. Transactional
domain publishers receive an opaque synchronous executor in SQLite and a
transaction-bound executor in PostgreSQL; they never receive a
`better-sqlite3` handle. The persistence-boundary test rejects native driver
handles in common job, resource, identity, chat, and Work contracts.

## Blob and vector storage foundation

Generated gallery media and document source files use `BlobStore`; document
RAG and persona memory use `VectorStore`. SQLite legacy gallery rows are
dual-read and adopted into blob references on first access. Relational
metadata and a durable reference are authoritative; provider URLs and physical
S3 keys are never persisted as application content. Chat attachments, avatars,
and other remaining inline binary fields are not yet blob-store callers and
must not be described as migrated.

The recovery gate authenticates every durable object and embedded-vector
envelope sequentially under explicit aggregate limits. It also strictly
authenticates recognizable legacy text envelopes and every saved-voice binary
envelope with the application `ENCRYPTION_KEY`; it never uses the runtime
decrypt-and-return-original compatibility fallback. It does not initialize,
repair, rewrite, or delete source storage; corrupt ciphertext, unknown or wrong
keys, non-canonical blob layouts, and exceeded verification limits block the
snapshot.

The default recovery bounds are 250,000 local objects, 64 GiB of encrypted and
plaintext blob bytes, 250,000 vector rows, 4 GiB of serialized vector
ciphertext, and 500 million vector components. Tests and embedded callers may
override per-run limits through `RecoveryInventoryOptions`; the CLI never
silently samples or skips excess state.

Legacy ciphertext verification defaults to one million populated candidate
fields and 16 GiB each of aggregate stored and authenticated plaintext bytes.
Plaintext rows from old schema generations remain compatible because legacy
text envelopes have no durable marker; the report counts only authenticated
envelopes. Saved voice envelopes are unambiguous and always authenticate with
profile/owner/field identity as additional data.

### Encrypted local blobs

`BlobStore` is owner-scoped and exposes streaming put/read, metadata/stat,
inclusive byte ranges, and idempotent deletion. `LocalEncryptedBlobStore`
writes opaque UUID-keyed objects below an application-supplied root; the
integration target is `${DATA_DIR}/blobs`. It uses exclusive staging files,
fsync, and an atomic same-filesystem rename, with `0700` directories and `0600`
files.

Every object has a random 256-bit data key. AES-256-GCM encrypts private
metadata and independently authenticates bounded body chunks. Additional
authenticated data binds the blob ID, owner, purpose, chunk index, and
plaintext length. The versioned storage keyring wraps each data key. The
descriptor records plaintext size, SHA-256, content type, creation time, format
version, and encryption key ID. Full reads verify SHA-256; range reads
authenticate every touched chunk.

The quota contract reserves capacity before streaming, consumes actual bytes,
commits only after atomic visibility, and releases failed reservations. SQLite
uses `BEGIN IMMEDIATE`; PostgreSQL uses serializable transactions and row
locks. S3 object metadata and quota usage commit or roll back in one database
transaction. Startup reconciles expired reservations and quota objects whose
physical blob is missing. `BLOB_QUOTA_BYTES_PER_USER` sets the durable per-owner
limit and `BLOB_QUOTA_RESERVATION_TTL_MS` bounds abandoned reservations.

`BLOB_STORE_BACKEND=s3` uses a private S3-compatible bucket. Libre uploads
opaque object keys and application-encrypted chunk streams, keeps encrypted
descriptors in PostgreSQL, supports inclusive HTTP ranges, verifies plaintext
and ciphertext SHA-256 digests, and performs idempotent deletion. A deleting
row remains durable until physical deletion and atomic metadata/quota removal
succeed; reconciliation retries interrupted deletes and removes aged physical
orphans. The Docker-gated MinIO suite covers cross-replica read/delete,
tenant isolation, quota contention, unconsumed streams, and injected database
failures at commit and deletion boundaries.

### Encrypted embedded vectors

`VectorStore` requires an actor on every query and mutation. Records carry a
namespace, opaque tenant-scoped ID, owner, resource ID, embedding model,
dimensions, version, source revision, equality attributes, and optional
user/group grants.

SQLite applies namespace/model/dimension/version, owner or grant, resource,
and attribute predicates before encrypted embeddings leave the database. Only
that bounded, authorized candidate set is decrypted and cosine-scored. The
same opaque vector ID is isolated per owner without revealing another tenant's
existence. Upserts replace embeddings, ACLs, and attributes atomically;
deletes are owner-scoped and cascade related rows.

Embeddings use AES-256-GCM with identity and model metadata bound as additional
authenticated data. Queryable identity, grant, model, version, revision, and
filter metadata remain plaintext, so callers must not put secrets in filter
attributes. Embeddings themselves are sensitive derived data.

`VECTOR_STORE_BACKEND=pgvector` applies namespace, model, dimensions, version,
resource, attribute, owner, and grant predicates inside the same SQL statement
as distance ordering and `LIMIT`. Post-filtering global nearest neighbors is
prohibited. Group authorization is resolved from a trusted current-membership
resolver for every query; caller-supplied `groupIds` are ignored. Revocation is
therefore immediate and forged group claims cannot retrieve candidates.

Document ingestion and the embedding-regeneration maintenance endpoint capture
one immutable execution specification before work begins: enabled state, model,
vector version, chunker version, chunk size, overlap, and similarity threshold.
That same specification controls chunk generation, relational publication,
vector upsert, and semantic query; a preference change during a run cannot
produce mixed-model chunks or query a vector under a different threshold.
The published document metadata records the aggregate chunk revision and this
specification so SQL remains the authoritative index manifest.

Regeneration holds an auto-renewing coordinator lease for each document and
rechecks the owner-scoped row plus its permanent deletion tombstone before
relational publication and before and after vector mutation. A deletion may
commit while an upsert is in flight; the post-upsert authority check then
removes the recreated vectors. PostgreSQL/team semantic reads never mutate
PGVector. SQLite may lazily republish relational embeddings only when the
stored manifest proves the exact current model and chunk configuration; that
optional mutation reloads the row and chunks while holding the same document
lease. A busy or superseded revision is skipped and remains eligible for
keyword fallback or an explicit regeneration.

Document indexes are replaced in compensated batches of at most 1,000 vectors,
and exact-index checks page through the complete resource manifest rather than
assuming one mutation batch is the whole document. A document may publish at
most 100,000 chunks, so no single document exceeds the portable archive's
total document-chunk ceiling. Ingestion rejects
the 100,001st chunk before embedding or relational/vector publication and
dead-letters that durable job without retry; increase the embedding chunk size
or remove excessive paragraph breaks before uploading again.

Pre-manifest solo databases can contain authenticated inline document vectors
without any record of the model or chunker that created them. First semantic
use treats only their presence as an upgrade signal: it rechunks authoritative
document text and generates every vector again under the current captured
specification while holding the document lease. It never copies the legacy
payload or labels it with today's preference. Provider failure or a busy lease
leaves the legacy row unchanged and keyword-searchable.

SQLite-to-team migration fails closed when such a legacy document is not fully
covered by authenticated current manifest metadata and an exact encrypted
platform-vector index. Current preferences do not prove a historical vector's
model. When dry-run reports this blocker, start the current release in
solo/SQLite mode with the same `DATA_DIR` and `ENCRYPTION_KEY`, enable and
select the desired embedding model, use **Settings -> Documents -> Regenerate
embeddings** for every affected owner, and rerun the migration dry-run. Only
then may team repository reads ignore the preserved inline ciphertext while
the proven vectors move to PGVector.

Confidentiality differs by backend. Embedded SQLite encrypts embeddings with
application AES-256-GCM after applying metadata ACL predicates. PGVector must
operate on the numeric embedding and therefore does not application-encrypt
that column. Treat embeddings as sensitive derived data: require TLS,
encrypted PostgreSQL volumes and backups, a least-privilege application role,
restricted database administration, and SQL logs that never include vector or
source content. Source text, persona memory content, gallery metadata, and blob
descriptors remain envelope-encrypted. Vector attributes are queryable
plaintext and must never contain secrets.

### Storage encryption keys

During the current caller-migration period, deployments that enable a versioned
keyring must set a stable 64-character `ENCRYPTION_KEY`, include that same key
under the exact `legacy` entry of `STORAGE_ENCRYPTION_KEYS`, and set
`STORAGE_ENCRYPTION_ACTIVE_KEY_ID` to one entry. Writes use the active key;
reads accept all configured key IDs to support staged rotation. This temporary
legacy requirement prevents the existing encryption service from independently
creating a different key. Retain old keys until every object and vector has
been rewritten or rewrapped and verified.

When that map is absent, the adapter accepts the existing 64-character
`ENCRYPTION_KEY` as key ID `legacy`, or reads the existing
`${DATA_DIR}/.encryption_key` file when the environment key is absent. The
storage factory only accepts a regular, non-symlinked key file with private
permissions; it never creates, rewrites, or replaces that file. If explicit
environment configuration conflicts with the persistent key, startup fails
closed. During rotation, a detected legacy environment/file key must remain in
the versioned map under the exact key ID `legacy` until old envelopes have been
rewritten and verified. Missing, malformed, mismatched, and unknown keys fail
closed.

Embedded vector queries apply ACL and metadata predicates in SQLite, then
aggregate candidate count, encrypted bytes, and count-by-dimension scoring work
before returning any ciphertext to Node for decryption. Queries exceeding any
budget fail closed and must be narrowed by resource or metadata scope.

## Coordination

The coordinator contract provides events, expiring cache entries, fenced
leases, and fixed-window rate-limit consumption. The local implementation is
only for the one-replica solo profile. The Redis implementation uses separate
command and subscription clients, bounded payloads, health checks, key
namespacing, atomic scripts, unique owner tokens, lease expiry, and fencing
tokens. It never falls back to local coordination after a Redis error.

Redis is not the source of truth. Authorization, durable jobs, and replayable
events must remain in the database; Redis is a wake-up, cache invalidation,
presence, quota, and coordination layer. Critical work must also validate its
database lease or fencing token before committing a side effect.

Application ticketing, caches, shared invalidation, connection limits, Work
events, and distributed runtime locks use this boundary. Redis selection alone
still does not make local persistence shareable; team mode requires the entire
shared profile.

## Durable jobs and events

SQLite migration v3 provides durable job, attempt, event-stream head, and
ordered event tables. The service contract supports idempotent enqueue,
bounded retries, cancellation, progress, lease heartbeat/reclaim, dead-letter
state, and replay by global cursor. Encrypted JSON payloads use the platform
keyring with job/event identity as authenticated data; payload references are
opaque bounded identifiers.

SQLite migration v13 and PostgreSQL migration v12 add the matching
`(stream_id, subject_id, global_cursor)` index used by generation-scoped chat
replay. Stream and subject filters are applied before the catch-up limit, so a
long session's earlier generations neither consume the current generation's
replay budget nor force a full event-stream scan.

Application and standalone-worker bootstraps register audited handlers for
document ingestion, media continuation, and retriable resource cleanup. The
admin boundary exposes bounded inspection and cancellation. Enqueue is
idempotent, and relational creation/deletion paths insert their durable job in
the same SQLite/PostgreSQL transaction. Resource cleanup removes vectors,
private blobs, durable references, cache entries, and resource-targeted queued
work through retry-safe operations.

The recovery inventory counts every job state and attempt outcome, records
event streams and their last cursor, blocks unsafe running work, and
authenticates encrypted payloads under aggregate limits. Recovery also rejects
stream-head mismatches and non-contiguous per-stream sequences.

Monotonic lease tokens fence stale workers from later database commits. Fencing
does not provide exactly-once execution: a worker can complete an external side
effect and fail before recording success. Handler adoption therefore requires
provider idempotency keys or a transactional outbox/inbox protocol, plus actor
authorization revalidation immediately before each side effect.

SQLite migration v4 adds a unique keyed email lookup token beside randomized
identity ciphertext. The token is an HMAC under the application encryption key:
it restores atomic duplicate-email enforcement without storing plaintext or
using deterministic encryption. Startup authenticates and backfills every
legacy identity email before accepting traffic.

## Health and recovery

Deployment probes now distinguish process liveness from dependency readiness:

- `/health` and `/health/live` are process-only;
- `/health/ready` checks the database, canonical schema ledger, writable data
  storage, and registered required dependencies while redacting details. It
  does not wait for optional providers; and
- `/health/deep` requires a current administrator and runs SQLite integrity and
  foreign-key checks in a bounded worker outside the HTTP event loop. It also
  aggregates optional server-level provider probes such as Ollama as warnings,
  without changing core readiness.

Run `libre-webui recovery-check --json`; from a source checkout, build the
backend once and use `npm run recovery:check -- --json`. The inventory is
read-only and reports schema/key identities, the local blob root,
legacy and platform-vector counts, authenticates local platform blob/vector
ciphertext, legacy application and saved-voice ciphertext, and encrypted
durable job/event payloads, and reports data sizes,
plugin definitions and whether they are inside the backup root, embedded media,
Work resources and exact ownership labels, job/attempt/event checkpoints,
active Work runs/previews and jobs, blockers, and known exclusions. It is a
pre-backup gate, not a complete backup. See
[Recovery Readiness](./44-RECOVERY_READINESS.md).

## Known remaining cutovers

The foundation does not imply that every binary field is already a blob.
Saved-voice audio, chat attachments, avatars, and future plugin-defined binary
resources need explicit reference metadata, dual-read/backfill, retention, and
deletion tests before they can move. Likewise, every future embedding caller
must carry model, dimensions, version, source revision, owner, resource scope,
and trusted grants through `VectorStore`; direct vector-table access is not an
accepted shortcut.

New long-running or externally visible side effects must register a durable
resource target, support cancellation and retry, and use a transactional
enqueue/outbox boundary with the owning relational mutation. Add each new
resource to the cross-replica upload/read/search/delete and backup/restore
acceptance gates before enabling it in team deployments.

---

# Global Capability Contracts

URL: https://docs.librewebui.org/global-capability-contracts

# Global Capability Contracts

<!-- Generated from scripts/global-capability-contracts.json. Do not edit this file by hand. -->

This executable inventory covers every explicit page route in `frontend/src/App.tsx`, every literal method/path declaration in every mounted `backend/src/routes/*.ts` Express router, and every literal WebSocket upgrade pathname in `backend/src/websocketServer.ts`. Each endpoint has exactly one capability owner, and every WebSocket endpoint has authentication evidence. Every capability also declares whether schemas, manifests, and defaults apply, with source evidence or a specific not-applicable reason, plus documentation and a named behavioral test registered in normal package or browser CI.

Executable provider details remain governed by `scripts/capability-contracts.json` and `scripts/test-capability-contracts.mjs`; this global inventory links to that contract instead of duplicating its manifest and default rules.

| Capability                        | UI routes                    | HTTP endpoints | WebSocket endpoints | Schemas                                                                                                      | Manifests                                                                                                            | Defaults                                                                                                                                                                 | Documentation                                                | Named behavioral tests                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| --------------------------------- | ---------------------------- | -------------: | ------------------: | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Home                              | `/`                          |              0 |                   0 | N/A — The Home page is a navigation and composition surface with no independent request or response payload. | N/A — The Home page is built into the application and is not installed or discovered through a manifest.             | `frontend/src/utils/welcomePrompts.ts`                                                                                                                                   | `docs/00-README.md`                                          | `new chats rotate through distinct creator prompts`<br />`frontend/e2e/welcome-prompts.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                                                   |
| Chat                              | `/chat`<br />`/c/:sessionId` |             27 |                   1 | `backend/src/types/index.ts`                                                                                 | `scripts/capability-contracts.json`                                                                                  | `backend/src/services/preferencesService.ts`                                                                                                                             | `docs/01-QUICK_START.md`                                     | `ChatRequestService prepares target, persona prompt, and shared messages`<br />`scripts/test-chat-context.mjs` (`test:package`)<br />`oversized history compacts into one summary on a user-turn boundary`<br />`scripts/test-context-compaction.mjs` (`test:package`)                                                                                                                                                                                                                                                         |
| Model management                  | `/models`                    |             48 |                   0 | `backend/src/types/index.ts`                                                                                 | `scripts/capability-contracts.json`                                                                                  | `backend/src/services/ollamaService.ts`                                                                                                                                  | `docs/02-WORKING_WITH_MODELS.md`                             | `cloud library pulls append the Ollama cloud suffix automatically`<br />`frontend/e2e/model-manager.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                                      |
| Personas                          | `/personas`                  |             12 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Personas are account resources persisted by Libre and do not use an installable or discovery manifest.         | `frontend/src/components/persona-form/defaults.ts`                                                                                                                       | `docs/16-PERSONA_DEVELOPMENT_FRAMEWORK.md`                   | `memory insert acknowledgement loss resolves the committed row and retry is idempotent`<br />`scripts/test-memory-reliability.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                            |
| Media gallery                     | `/gallery`                   |             19 |                   0 | `backend/src/types/index.ts`                                                                                 | `scripts/capability-contracts.json`                                                                                  | `scripts/test-capability-contracts.mjs`                                                                                                                                  | `docs/38-MEDIA_GENERATION.md`                                | `video jobs can stop waiting, reopen, and resume without losing the handle`<br />`frontend/e2e/media-generation.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                          |
| Notes                             | `/notes`                     |              4 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Notes are first-party account resources and have no installable or discovery manifest.                         | N/A — Notes have no implicit content or presentation default beyond explicit user-supplied fields.                                                                       | `docs/03-PRO_TIPS.md`                                        | `notes open in Markdown preview and make editing explicit`<br />`frontend/e2e/notes.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                                                      |
| Calendar                          | `/calendar`                  |              4 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Calendar events are first-party account resources and have no installable or discovery manifest.               | N/A — Calendar events have no implicit content default beyond explicit user-supplied fields; recurrence math uses the server timezone documented in docs/47-CALENDAR.md. | `docs/47-CALENDAR.md`                                        | `calendar events persist per owner and expand recurrence into range queries`<br />`scripts/test-calendar.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                 |
| Automations                       | `/automations`               |             12 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Automations are first-party account resources and have no installable or discovery manifest.                   | `docs/48-AUTOMATIONS.md`                                                                                                                                                 | `docs/48-AUTOMATIONS.md`                                     | `the scheduler fires due automations once and settles stalled runs`<br />`scripts/test-automations.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                       |
| Work                              | `/work`<br />`/work/:taskId` |             29 |                   1 | `backend/src/types/work.ts`                                                                                  | `scripts/capability-contracts.json`                                                                                  | `backend/src/services/workRuntimeShared.ts`                                                                                                                              | `docs/33-WORKSPACES.md`                                      | `runtime limits expose admission capacity and live occupancy`<br />`scripts/test-work-runtime.mjs` (`test:package`)<br />`production Work upgrade applies task-bound ticket authentication`<br />`scripts/test-websocket-tickets.mjs` (`test:package`)                                                                                                                                                                                                                                                                         |
| Agent CLI models                  | `/agents`                    |              1 |                   0 | `backend/src/services/agentCliService.ts`                                                                    | N/A — Agent CLI adapters are compiled first-party definitions and are not loaded from provider manifests.            | `backend/src/services/agentAccessService.ts`                                                                                                                             | `docs/36-AGENT_CLI_MODELS.md`                                | `every agent CLI passes an explicit model through to its argv`<br />`scripts/test-agent-cli-chat.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                         |
| Artifacts                         | `/artifacts`                 |              1 |                   0 | `frontend/src/utils/artifactParser.ts`                                                                       | `frontend/src/artifact-runtime/manifest.ts`                                                                          | `frontend/src/components/ArtifactSlideOutPanel.tsx`                                                                                                                      | `docs/13-ARTIFACTS_FEATURE.md`                               | `chat detects multi-file HTML artifacts and renders them in the slide-out panel`<br />`frontend/e2e/artifacts.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                            |
| Provider usage                    | `/usage`                     |              1 |                   0 | `backend/src/services/pluginUsageService.ts`                                                                 | N/A — Usage analytics consume provider events but define no installable or discovery manifest of their own.          | `backend/src/services/pluginUsageService.ts`                                                                                                                             | `docs/37-SYSTEM_MONITORING.md`                               | `the usage endpoint is explicitly protected by administrator middleware`<br />`scripts/test-plugin-usage-analytics.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                       |
| System diagnostics                | `/system`                    |              5 |                   0 | `backend/src/services/systemDiagnosticsService.ts`                                                           | N/A — System diagnostics are a built-in authenticated report and have no installable or discovery manifest.          | `backend/src/services/healthService.ts`                                                                                                                                  | `docs/37-SYSTEM_MONITORING.md`                               | `system route requires both authentication and current administrator status`<br />`scripts/test-system-diagnostics.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                       |
| User administration               | `/users`                     |              7 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — User administration is a built-in identity capability and has no installable or discovery manifest.            | N/A — User role and account status are selected by bootstrap or approval policy rather than a reusable capability default.                                               | `docs/12-AUTHENTICATION.md`                                  | `public registrations require administrator approval before authentication`<br />`scripts/test-user-approval.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                             |
| Authentication                    | `/login`                     |             24 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Authentication is a built-in security boundary and is not installed or discovered through a manifest.          | `backend/src/services/registrationPolicy.ts`                                                                                                                             | `docs/12-AUTHENTICATION.md`<br />`docs/12-AUTHENTICATION.md` | `password login requires and submits a Turnstile token`<br />`frontend/e2e/auth.spec.ts` (`test:e2e`)<br />`revoking the session rejects its token with 401`<br />`scripts/test-auth-sessions.mjs` (`test:package`)<br />`scope-to-path matrix: notes scope on notes, chat, and auth routes`<br />`scripts/test-api-tokens.mjs` (`test:package`)<br />`verifyIdToken accepts a good token and returns its claims`<br />`scripts/test-oidc-provider.mjs` (`test:package`)                                                       |
| Access Control                    | —                            |             12 |                   0 | `backend/src/persistence/securityTypes.ts`                                                                   | N/A — Access control is a built-in security boundary and is not installed or discovered through a manifest.          | `backend/src/services/authorizationService.ts`                                                                                                                           | `docs/12-AUTHENTICATION.md`                                  | `group grants reach members and die with the membership`<br />`scripts/test-authorization.mjs` (`test:package`)<br />`a rolled-back duplicate group create leaves no audit row`<br />`scripts/test-security-audit.mjs` (`test:package`)                                                                                                                                                                                                                                                                                        |
| Data portability                  | —                            |              3 |                   0 | `backend/src/services/dataArchiveService.ts`                                                                 | `backend/src/services/dataArchiveService.ts`                                                                         | `backend/src/services/dataArchiveService.ts`                                                                                                                             | `docs/41-DATA_PORTABILITY.md`                                | `v3 export is complete, checksummed, user-scoped, and explicit about exclusions`<br />`scripts/test-data-archive.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                         |
| Document knowledge and retrieval  | —                            |             15 |                   0 | `backend/src/types/index.ts`                                                                                 | `scripts/capability-contracts.json`                                                                                  | `backend/src/services/preferencesService.ts`                                                                                                                             | `docs/09-RAG_FEATURE.md`                                     | `keyword retrieval sees session documents AND user-scoped uploads`<br />`scripts/test-rag-retrieval.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                      |
| Persona memory                    | —                            |              6 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Persona memory is an internal persona resource and has no installable or discovery manifest.                   | `frontend/src/components/persona-form/defaults.ts`                                                                                                                       | `docs/16-PERSONA_DEVELOPMENT_FRAMEWORK.md`                   | `persona deletion during vector publication still removes the recreated vector`<br />`scripts/test-memory-reliability.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                    |
| Web search                        | —                            |              5 |                   0 | `backend/src/services/webSearchService.ts`                                                                   | N/A — Web search uses administrator connection settings rather than an installable provider manifest.                | `backend/src/services/webSearchService.ts`                                                                                                                               | `docs/39-WEB_SEARCH.md`                                      | `web search ships disabled and validates its configuration`<br />`scripts/test-web-search.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                                |
| Settings and preferences          | —                            |              9 |                   0 | `backend/src/types/index.ts`                                                                                 | N/A — Settings aggregate first-party and provider configuration but do not define a separate installable manifest.   | `backend/src/services/preferencesService.ts`                                                                                                                             | `docs/35-PROVIDER_CONNECTIONS.md`                            | `theme preference survives refresh and retries a failed save`<br />`frontend/e2e/settings.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                                                |
| Speech input and output           | —                            |             10 |                   0 | `backend/src/types/index.ts`                                                                                 | `scripts/capability-contracts.json`                                                                                  | `scripts/test-capability-contracts.mjs`                                                                                                                                  | `docs/42-SPEECH_TO_TEXT.md`<br />`docs/27-QWEN3_TTS.md`      | `STT sends an OpenAI-compatible multipart request to the selected route`<br />`scripts/test-stt-routing.mjs` (`test:package`)<br />`provider speech input discloses its route and transcribes recorded audio`<br />`frontend/e2e/stt.spec.ts` (`test:e2e`)<br />`invalid provider audio is rejected safely without inserting a transcript`<br />`frontend/e2e/stt.spec.ts` (`test:e2e`)<br />`TTS routes a shared model alias through the selected plugin and user valve`<br />`scripts/test-tts-routing.mjs` (`test:package`) |
| Hugging Face Hub                  | —                            |              7 |                   0 | `backend/src/routes/huggingfaceHub.ts`                                                                       | N/A — Hub model metadata is remote API data; Libre defines no local installable manifest for the browser capability. | `backend/src/routes/huggingfaceHub.ts`                                                                                                                                   | `docs/29-HUGGINGFACE_HUB.md`                                 | `Hugging Face Hub task discovery is authenticated route behavior`<br />`scripts/test-huggingface-hub-routes.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                              |
| Libre Claw                        | —                            |             26 |                   0 | `backend/src/services/libreClawService.ts`                                                                   | N/A — Libre Claw is an explicitly configured service integration and does not use a Libre provider manifest.         | `backend/src/services/libreClawService.ts`                                                                                                                               | `docs/31-LIBRE_CLAW_INTEGRATION.md`                          | `Work and Agents are hidden and route-protected for non-admin users`<br />`frontend/e2e/sidebar-create-actions.spec.ts` (`test:e2e`)                                                                                                                                                                                                                                                                                                                                                                                           |
| Durable jobs and events           | —                            |              4 |                   0 | `backend/src/platform/jobs/durableJobTypes.ts`                                                               | N/A — Durable jobs use registered handler types and persisted payload contracts, not installable manifests.          | `backend/src/platform/jobs/embeddedDurableJobWorker.ts`                                                                                                                  | `docs/45-PLATFORM_FOUNDATION.md`                             | `ordered event replay uses global cursors and per-stream sequences`<br />`scripts/test-durable-jobs.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                                      |
| Solo and team deployment profiles | —                            |              0 |                   0 | `backend/src/platform/runtimeConfig.ts`                                                                      | `helm/libre-webui/values.schema.json`                                                                                | `helm/libre-webui/values.yaml`                                                                                                                                           | `docs/45-PLATFORM_FOUNDATION.md`                             | `real Redis coordinates independent replicas and fails closed after shutdown`<br />`scripts/test-platform-coordination.mjs` (`test:package`)                                                                                                                                                                                                                                                                                                                                                                                   |
| Recovery and backup               | —                            |              0 |                   0 | `backend/src/services/recoveryInventoryService.ts`                                                           | `backend/src/platform/recovery/backupArchive.ts`                                                                     | `backend/src/services/recoveryInventoryService.ts`                                                                                                                       | `docs/44-RECOVERY_READINESS.md`                              | `integrated backup is signed, encrypted, tamper-evident, and clean-restorable`<br />`scripts/test-integrated-backup.mjs` (`test:package`)<br />`team backup restores a clean PostgreSQL and versioned S3 target with authenticated PGVector state`<br />`scripts/test-team-backup.mjs` (`test:package`)                                                                                                                                                                                                                        |

## Exact backend route inventory

### Home

No mounted Express endpoint. The executable boundary is the UI and/or source evidence recorded above.

### Chat

- `DELETE /api/chat/folders/:folderId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/folders/:folderId`)
- `DELETE /api/chat/sessions` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions`)
- `DELETE /api/chat/sessions/:sessionId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId`)
- `GET /api/chat/compaction-config` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/compaction-config`)
- `GET /api/chat/context-policy` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/context-policy`)
- `GET /api/chat/folders` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/folders`)
- `GET /api/chat/sessions` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions`)
- `GET /api/chat/sessions/:sessionId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId`)
- `GET /api/chat/sessions/:sessionId/events` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/events`)
- `GET /api/chat/sessions/:sessionId/messages/:messageId/branches` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages/:messageId/branches`)
- `POST /api/chat/folders` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/folders`)
- `POST /api/chat/sessions` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions`)
- `POST /api/chat/sessions/:sessionId/compaction/:messageId/restore` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/compaction/:messageId/restore`)
- `POST /api/chat/sessions/:sessionId/followups` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/followups`)
- `POST /api/chat/sessions/:sessionId/generate` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/generate`)
- `POST /api/chat/sessions/:sessionId/generate-title` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/generate-title`)
- `POST /api/chat/sessions/:sessionId/generate/stream` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/generate/stream`)
- `POST /api/chat/sessions/:sessionId/generations` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/generations`)
- `POST /api/chat/sessions/:sessionId/generations/:assistantMessageId/cancel` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/generations/:assistantMessageId/cancel`)
- `POST /api/chat/sessions/:sessionId/messages` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages`)
- `POST /api/chat/sessions/:sessionId/messages/:messageId/branch` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages/:messageId/branch`)
- `POST /api/chat/sessions/:sessionId/messages/:messageId/branches` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages/:messageId/branches`)
- `POST /api/chat/sessions/:sessionId/messages/:messageId/truncate` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages/:messageId/truncate`)
- `PUT /api/chat/compaction-config` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/compaction-config`)
- `PUT /api/chat/folders/:folderId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/folders/:folderId`)
- `PUT /api/chat/sessions/:sessionId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId`)
- `PUT /api/chat/sessions/:sessionId/messages/:messageId` — `backend/src/routes/chat.ts` (mount `/api/chat`, subpath `/sessions/:sessionId/messages/:messageId`)

### Model management

- `DELETE /api/ollama/models` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models`)
- `GET /api/ollama/health` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/health`)
- `GET /api/ollama/library` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/library`)
- `GET /api/ollama/models` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models`)
- `GET /api/ollama/models/:modelName/defaults` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/:modelName/defaults`)
- `GET /api/ollama/models/access` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/access`)
- `GET /api/ollama/models/pull-all/stream` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/pull-all/stream`)
- `GET /api/ollama/models/show` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/show`)
- `GET /api/ollama/models/visibility` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/visibility`)
- `GET /api/ollama/pull/stream` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/pull/stream`)
- `GET /api/ollama/running` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/running`)
- `GET /api/ollama/version` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/version`)
- `HEAD /api/ollama/blobs/:digest` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/blobs/:digest`)
- `POST /api/ollama/blobs/:digest` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/blobs/:digest`)
- `POST /api/ollama/chat` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/chat`)
- `POST /api/ollama/chat/stream` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/chat/stream`)
- `POST /api/ollama/embed` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/embed`)
- `POST /api/ollama/embeddings` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/embeddings`)
- `POST /api/ollama/models` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models`)
- `POST /api/ollama/models/copy` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/copy`)
- `POST /api/ollama/models/pull` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/pull`)
- `POST /api/ollama/models/pull-all` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/pull-all`)
- `POST /api/ollama/models/push` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/push`)
- `POST /api/ollama/models/unload` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/unload`)
- `POST /api/ollama/models/unload-all` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/unload-all`)
- `PUT /api/ollama/models/access` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/access`)
- `PUT /api/ollama/models/visibility` — `backend/src/routes/ollama.ts` (mount `/api/ollama`, subpath `/models/visibility`)
- `DELETE /api/plugins/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id`)
- `DELETE /api/plugins/:id/credentials` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/credentials`)
- `DELETE /api/plugins/:id/variables` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/variables`)
- `GET /api/plugins` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/`)
- `GET /api/plugins/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id`)
- `GET /api/plugins/:id/credentials/check` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/credentials/check`)
- `GET /api/plugins/:id/export` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/export`)
- `GET /api/plugins/:id/variables` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/variables`)
- `GET /api/plugins/active` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/active`)
- `GET /api/plugins/active/current` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/active/current`)
- `GET /api/plugins/credentials/all` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/credentials/all`)
- `GET /api/plugins/status/all` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/status/all`)
- `POST /api/plugins/:id/credentials` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/credentials`)
- `POST /api/plugins/activate/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/activate/:id`)
- `POST /api/plugins/deactivate` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/deactivate`)
- `POST /api/plugins/deactivate/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/deactivate/:id`)
- `POST /api/plugins/discover/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/discover/:id`)
- `POST /api/plugins/install` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/install`)
- `POST /api/plugins/upload` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/upload`)
- `PUT /api/plugins/:id` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id`)
- `PUT /api/plugins/:id/variables` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/:id/variables`)

### Personas

- `DELETE /api/personas/:id` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id`)
- `GET /api/personas` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/`)
- `GET /api/personas/:id` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id`)
- `GET /api/personas/:id/backup` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/backup`)
- `GET /api/personas/:id/download` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/download`)
- `GET /api/personas/:id/export` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/export`)
- `GET /api/personas/:id/export/dna` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/export/dna`)
- `GET /api/personas/defaults/parameters` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/defaults/parameters`)
- `GET /api/personas/stats/count` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/stats/count`)
- `POST /api/personas` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/`)
- `POST /api/personas/import` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/import`)
- `PUT /api/personas/:id` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id`)

### Media gallery

- `DELETE /api/image-gen/gallery/:imageId` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/gallery/:imageId`)
- `GET /api/image-gen/config/:pluginId` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/config/:pluginId`)
- `GET /api/image-gen/gallery` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/gallery`)
- `GET /api/image-gen/gallery/:imageId` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/gallery/:imageId`)
- `GET /api/image-gen/models` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/models`)
- `GET /api/image-gen/plugins` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/plugins`)
- `POST /api/image-gen/generate` — `backend/src/routes/imageGen.ts` (mount `/api/image-gen`, subpath `/generate`)
- `DELETE /api/media/gallery/:mediaId` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/gallery/:mediaId`)
- `DELETE /api/media/video/jobs/:jobId` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/video/jobs/:jobId`)
- `GET /api/media/gallery` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/gallery`)
- `GET /api/media/gallery/:mediaId/content` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/gallery/:mediaId/content`)
- `GET /api/media/models` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/models`)
- `GET /api/media/video/jobs` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/video/jobs`)
- `GET /api/media/video/jobs/:jobId` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/video/jobs/:jobId`)
- `POST /api/media/audio/generate` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/audio/generate`)
- `POST /api/media/audio/voice-clone` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/audio/voice-clone`)
- `POST /api/media/sound/generate` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/sound/generate`)
- `POST /api/media/video/generate` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/video/generate`)
- `POST /api/media/video/jobs/:jobId/resume` — `backend/src/routes/media.ts` (mount `/api/media`, subpath `/video/jobs/:jobId/resume`)

### Notes

- `DELETE /api/notes/:noteId` — `backend/src/routes/notes.ts` (mount `/api/notes`, subpath `/:noteId`)
- `GET /api/notes` — `backend/src/routes/notes.ts` (mount `/api/notes`, subpath `/`)
- `POST /api/notes` — `backend/src/routes/notes.ts` (mount `/api/notes`, subpath `/`)
- `PUT /api/notes/:noteId` — `backend/src/routes/notes.ts` (mount `/api/notes`, subpath `/:noteId`)

### Calendar

- `DELETE /api/calendar/events/:eventId` — `backend/src/routes/calendar.ts` (mount `/api/calendar`, subpath `/events/:eventId`)
- `GET /api/calendar/events` — `backend/src/routes/calendar.ts` (mount `/api/calendar`, subpath `/events`)
- `POST /api/calendar/events` — `backend/src/routes/calendar.ts` (mount `/api/calendar`, subpath `/events`)
- `PUT /api/calendar/events/:eventId` — `backend/src/routes/calendar.ts` (mount `/api/calendar`, subpath `/events/:eventId`)

### Automations

- `DELETE /api/automations/:automationId` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId`)
- `GET /api/automations` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/`)
- `GET /api/automations/:automationId` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId`)
- `GET /api/automations/occurrences` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/occurrences`)
- `GET /api/automations/runs` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/runs`)
- `GET /api/automations/runs/summary` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/runs/summary`)
- `POST /api/automations` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/`)
- `POST /api/automations/:automationId/pause` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId/pause`)
- `POST /api/automations/:automationId/resume` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId/resume`)
- `POST /api/automations/:automationId/run` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId/run`)
- `POST /api/automations/runs/seen` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/runs/seen`)
- `PUT /api/automations/:automationId` — `backend/src/routes/automations.ts` (mount `/api/automations`, subpath `/:automationId`)

### Work

- `DELETE /api/work/policies/:id` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/policies/:id`)
- `DELETE /api/work/tasks/:id` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id`)
- `GET /api/work/access` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/access`)
- `GET /api/work/admin/overview` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/admin/overview`)
- `GET /api/work/capabilities` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/capabilities`)
- `GET /api/work/policies` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/policies`)
- `GET /api/work/tasks` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks`)
- `GET /api/work/tasks/:id` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id`)
- `GET /api/work/tasks/:id/file` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/file`)
- `GET /api/work/tasks/:id/files` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/files`)
- `GET /api/work/tasks/:id/git` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git`)
- `GET /api/work/tasks/:id/git/diff` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/diff`)
- `GET /api/work/tasks/:id/messages` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/messages`)
- `GET /api/work/tasks/:taskId/runs/:runId/events` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:taskId/runs/:runId/events`)
- `PATCH /api/work/tasks/:id` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id`)
- `POST /api/work/policies` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/policies`)
- `POST /api/work/tasks` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks`)
- `POST /api/work/tasks/:id/cancel` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/cancel`)
- `POST /api/work/tasks/:id/git/branches` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/branches`)
- `POST /api/work/tasks/:id/git/commit` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/commit`)
- `POST /api/work/tasks/:id/git/init` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/init`)
- `POST /api/work/tasks/:id/git/stage` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/stage`)
- `POST /api/work/tasks/:id/git/switch` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/git/switch`)
- `POST /api/work/tasks/:id/preview/start` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/preview/start`)
- `POST /api/work/tasks/:id/preview/stop` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/preview/stop`)
- `POST /api/work/tasks/:id/runs` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/runs`)
- `PUT /api/work/access` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/access`)
- `PUT /api/work/policies/:id` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/policies/:id`)
- `PUT /api/work/tasks/:id/file` — `backend/src/routes/work.ts` (mount `/api/work`, subpath `/tasks/:id/file`)

### Agent CLI models

- `GET /api/agent-clis/models` — `backend/src/routes/agentCli.ts` (mount `/api/agent-clis`, subpath `/models`)

### Artifacts

- `GET /api/artifacts/sandbox` — `backend/src/routes/artifacts.ts` (mount `/api/artifacts`, subpath `/sandbox`)

### Provider usage

- `GET /api/plugins/usage` — `backend/src/routes/plugins.ts` (mount `/api/plugins`, subpath `/usage`)

### System diagnostics

- `GET /health` — `backend/src/routes/health.ts` (mount `/health`, subpath `/`)
- `GET /health/deep` — `backend/src/routes/health.ts` (mount `/health`, subpath `/deep`)
- `GET /health/live` — `backend/src/routes/health.ts` (mount `/health`, subpath `/live`)
- `GET /health/ready` — `backend/src/routes/health.ts` (mount `/health`, subpath `/ready`)
- `GET /api/system` — `backend/src/routes/systemDiagnostics.ts` (mount `/api/system`, subpath `/`)

### User administration

- `DELETE /api/users/:id` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/:id`)
- `GET /api/users` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/`)
- `GET /api/users/pending-approvals` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/pending-approvals`)
- `PATCH /api/users/:id` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/:id`)
- `PATCH /api/users/:id/approve` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/:id/approve`)
- `PATCH /api/users/me/avatar` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/me/avatar`)
- `POST /api/users` — `backend/src/routes/users.ts` (mount `/api/users`, subpath `/`)

### Authentication

- `DELETE /api/auth/sessions/:id` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/sessions/:id`)
- `DELETE /api/auth/tokens/:id` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/tokens/:id`)
- `GET /api/auth/encryption-key` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/encryption-key`)
- `GET /api/auth/me` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/me`)
- `GET /api/auth/oauth/github` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/github`)
- `GET /api/auth/oauth/github/callback` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/github/callback`)
- `GET /api/auth/oauth/github/status` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/github/status`)
- `GET /api/auth/oauth/huggingface` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/huggingface`)
- `GET /api/auth/oauth/huggingface/callback` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/huggingface/callback`)
- `GET /api/auth/oauth/huggingface/status` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/huggingface/status`)
- `GET /api/auth/oauth/oidc` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/oidc`)
- `GET /api/auth/oauth/oidc/callback` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/oidc/callback`)
- `GET /api/auth/oauth/oidc/status` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/oidc/status`)
- `GET /api/auth/sessions` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/sessions`)
- `GET /api/auth/system-info` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/system-info`)
- `GET /api/auth/tokens` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/tokens`)
- `GET /api/auth/verify` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/verify`)
- `POST /api/auth/login` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/login`)
- `POST /api/auth/logout` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/logout`)
- `POST /api/auth/oauth/exchange` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/oauth/exchange`)
- `POST /api/auth/sessions/revoke-others` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/sessions/revoke-others`)
- `POST /api/auth/signup` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/signup`)
- `POST /api/auth/tokens` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/tokens`)
- `POST /api/auth/websocket-ticket` — `backend/src/routes/auth.ts` (mount `/api/auth`, subpath `/websocket-ticket`)

### Access Control

- `DELETE /api/access/grants/:id` — `backend/src/routes/access.ts` (mount `/api/access`, subpath `/grants/:id`)
- `GET /api/access/grants` — `backend/src/routes/access.ts` (mount `/api/access`, subpath `/grants`)
- `GET /api/access/shared-with-me` — `backend/src/routes/access.ts` (mount `/api/access`, subpath `/shared-with-me`)
- `POST /api/access/grants` — `backend/src/routes/access.ts` (mount `/api/access`, subpath `/grants`)
- `GET /api/audit` — `backend/src/routes/audit.ts` (mount `/api/audit`, subpath `/`)
- `DELETE /api/groups/:id` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/:id`)
- `DELETE /api/groups/:id/members/:userId` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/:id/members/:userId`)
- `GET /api/groups` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/`)
- `GET /api/groups/effective/:userId` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/effective/:userId`)
- `PATCH /api/groups/:id` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/:id`)
- `POST /api/groups` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/`)
- `POST /api/groups/:id/members` — `backend/src/routes/groups.ts` (mount `/api/groups`, subpath `/:id/members`)

### Data portability

- `GET /api/preferences/export` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/export`)
- `POST /api/preferences/import` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/import`)
- `POST /api/preferences/import/preflight` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/import/preflight`)

### Document knowledge and retrieval

- `DELETE /api/documents/:documentId` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/:documentId`)
- `DELETE /api/documents/collections/:collectionId` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/collections/:collectionId`)
- `GET /api/documents` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/`)
- `GET /api/documents/:documentId` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/:documentId`)
- `GET /api/documents/:documentId/source` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/:documentId/source`)
- `GET /api/documents/collections` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/collections`)
- `GET /api/documents/embeddings/status` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/embeddings/status`)
- `GET /api/documents/session/:sessionId` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/session/:sessionId`)
- `POST /api/documents/collections` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/collections`)
- `POST /api/documents/embeddings/regenerate` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/embeddings/regenerate`)
- `POST /api/documents/fetch-url` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/fetch-url`)
- `POST /api/documents/search` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/search`)
- `POST /api/documents/upload` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/upload`)
- `PUT /api/documents/:documentId/collection` — `backend/src/routes/documents.ts` (mount `/api/documents`, subpath `/:documentId/collection`)
- `GET /api/embeddings/models` — `backend/src/routes/embeddings.ts` (mount `/api/embeddings`, subpath `/models`)

### Persona memory

- `DELETE /api/personas/:id/memory` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory`)
- `GET /api/personas/:id/memory/core` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory/core`)
- `GET /api/personas/:id/memory/stats` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory/stats`)
- `GET /api/personas/:id/memory/status` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory/status`)
- `POST /api/personas/:id/memory/consolidate` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory/consolidate`)
- `POST /api/personas/:id/memory/decay` — `backend/src/routes/personas.ts` (mount `/api/personas`, subpath `/:id/memory/decay`)

### Web search

- `GET /api/search/access` — `backend/src/routes/search.ts` (mount `/api/search`, subpath `/access`)
- `GET /api/search/config` — `backend/src/routes/search.ts` (mount `/api/search`, subpath `/config`)
- `POST /api/search/test` — `backend/src/routes/search.ts` (mount `/api/search`, subpath `/test`)
- `PUT /api/search/access` — `backend/src/routes/search.ts` (mount `/api/search`, subpath `/access`)
- `PUT /api/search/config` — `backend/src/routes/search.ts` (mount `/api/search`, subpath `/config`)

### Settings and preferences

- `GET /api/preferences` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/`)
- `POST /api/preferences/embedding-settings/reset` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/embedding-settings/reset`)
- `POST /api/preferences/generation-options/reset` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/generation-options/reset`)
- `PUT /api/preferences` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/`)
- `PUT /api/preferences/default-model` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/default-model`)
- `PUT /api/preferences/embedding-settings` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/embedding-settings`)
- `PUT /api/preferences/generation-options` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/generation-options`)
- `PUT /api/preferences/model-generation-options` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/model-generation-options`)
- `PUT /api/preferences/system-message` — `backend/src/routes/preferences.ts` (mount `/api/preferences`, subpath `/system-message`)

### Speech input and output

- `GET /api/stt/models` — `backend/src/routes/stt.ts` (mount `/api/stt`, subpath `/models`)
- `POST /api/stt/transcribe` — `backend/src/routes/stt.ts` (mount `/api/stt`, subpath `/transcribe`)
- `DELETE /api/tts/voice-profiles/:profileId` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/voice-profiles/:profileId`)
- `GET /api/tts/models` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/models`)
- `GET /api/tts/plugins` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/plugins`)
- `GET /api/tts/voice-profiles` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/voice-profiles`)
- `GET /api/tts/voices/:pluginId` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/voices/:pluginId`)
- `POST /api/tts/generate` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/generate`)
- `POST /api/tts/generate-base64` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/generate-base64`)
- `POST /api/tts/voice-clone` — `backend/src/routes/tts.ts` (mount `/api/tts`, subpath `/voice-clone`)

### Hugging Face Hub

- `GET /api/huggingface-hub/models` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/models`)
- `GET /api/huggingface-hub/models/:author/:modelName` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/models/:author/:modelName`)
- `GET /api/huggingface-hub/models/:author/:modelName/gguf` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/models/:author/:modelName/gguf`)
- `GET /api/huggingface-hub/models/:author/:modelName/has-gguf` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/models/:author/:modelName/has-gguf`)
- `GET /api/huggingface-hub/tasks` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/tasks`)
- `GET /api/huggingface-hub/trending` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/trending`)
- `POST /api/huggingface-hub/cache/clear` — `backend/src/routes/huggingfaceHub.ts` (mount `/api/huggingface-hub`, subpath `/cache/clear`)

### Libre Claw

- `DELETE /api/libre-claw/automations/:automationId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId`)
- `GET /api/libre-claw/access` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/access`)
- `GET /api/libre-claw/automations` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations`)
- `GET /api/libre-claw/automations/:automationId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId`)
- `GET /api/libre-claw/config/fallback` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/config/fallback`)
- `GET /api/libre-claw/config/model` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/config/model`)
- `GET /api/libre-claw/dashboard` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/dashboard`)
- `GET /api/libre-claw/health` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/health`)
- `GET /api/libre-claw/runs` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs`)
- `GET /api/libre-claw/runs/:runId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs/:runId`)
- `GET /api/libre-claw/runs/:runId/events` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs/:runId/events`)
- `GET /api/libre-claw/status` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/status`)
- `GET /api/libre-claw/usage` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/usage`)
- `PATCH /api/libre-claw/automations/:automationId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId`)
- `PATCH /api/libre-claw/config/fallback` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/config/fallback`)
- `PATCH /api/libre-claw/config/model` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/config/model`)
- `PATCH /api/libre-claw/config/theme` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/config/theme`)
- `POST /api/libre-claw/automations` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations`)
- `POST /api/libre-claw/automations/:automationId/pause` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId/pause`)
- `POST /api/libre-claw/automations/:automationId/resume` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId/resume`)
- `POST /api/libre-claw/automations/:automationId/run` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId/run`)
- `POST /api/libre-claw/runs` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs`)
- `POST /api/libre-claw/runs/:runId/cancel` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs/:runId/cancel`)
- `POST /api/libre-claw/runs/:runId/permissions/:toolCallId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/runs/:runId/permissions/:toolCallId`)
- `PUT /api/libre-claw/access` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/access`)
- `PUT /api/libre-claw/automations/:automationId` — `backend/src/routes/libreClaw.ts` (mount `/api/libre-claw`, subpath `/automations/:automationId`)

### Durable jobs and events

- `GET /api/jobs` — `backend/src/routes/jobs.ts` (mount `/api/jobs`, subpath `/`)
- `GET /api/jobs/:jobId` — `backend/src/routes/jobs.ts` (mount `/api/jobs`, subpath `/:jobId`)
- `GET /api/jobs/:jobId/events` — `backend/src/routes/jobs.ts` (mount `/api/jobs`, subpath `/:jobId/events`)
- `POST /api/jobs/:jobId/cancel` — `backend/src/routes/jobs.ts` (mount `/api/jobs`, subpath `/:jobId/cancel`)

### Solo and team deployment profiles

No mounted Express endpoint. The executable boundary is the UI and/or source evidence recorded above.

### Recovery and backup

No mounted Express endpoint. The executable boundary is the UI and/or source evidence recorded above.

## Exact WebSocket route inventory

- `WEBSOCKET /ws` — **Chat**, `backend/src/websocketServer.ts`; authentication evidence: `backend/src/websocketServer.ts`, `scripts/test-websocket-tickets.mjs`
- `WEBSOCKET /ws/work-terminal` — **Work**, `backend/src/websocketServer.ts`; authentication evidence: `backend/src/workTerminalServer.ts`, `scripts/test-websocket-tickets.mjs`

## Enforcement boundary

The package gate parses TypeScript/JavaScript source, resolves every imported Express router to its literal application mount, and reconciles every literal router method/path declaration and WebSocket upgrade pathname one-for-one with this inventory. It fails on an unmounted route file, an unsupported `router.route()` builder, a dynamic route path, a missing or duplicate endpoint owner, missing WebSocket authentication evidence, a changed UI route, missing schema/manifest/default applicability, stale evidence, or a stale generated document.

Focused-test evidence is scoped to one exact named `test(...)` declaration and at least two behavior patterns inside that test body. Backend tests must be exact file arguments to `test:package`; Playwright specs must match normal discovery. The gate also proves that both runners execute in the normal pull-request and `dev`/`main` Format & Lint workflow.

---

# Calendar

URL: https://docs.librewebui.org/calendar

# Calendar

Libre WebUI includes a personal calendar. Every signed-in user gets their own
month and week views with one-off and recurring events, and scheduled
automations project their upcoming occurrences and run history onto the same
grid.

## Events

An event has a title, optional notes, a start time, an optional end time, an
all-day flag, and an optional recurrence rule. Titles and notes are encrypted
at rest with the same envelope used for chats and notes.

Recurrence uses the shared trigger model:

| Kind      | Fields                                  | Meaning                        |
| --------- | --------------------------------------- | ------------------------------ |
| `once`    | `at`                                    | A single occurrence            |
| `hourly`  | `minute`, `startHour?`, `endHour?`      | Every hour within a window     |
| `daily`   | `hour`, `minute`                        | Every day at a wall-clock time |
| `weekly`  | `dayOfWeek`, `hour`, `minute`           | Every week on a chosen day     |
| `monthly` | `dayOfMonth`, `hour`, `minute`          | Every month on a chosen day    |
| `yearly`  | `month`, `dayOfMonth`, `hour`, `minute` | Every year on a chosen date    |

Recurring events are stored once. Range queries expand them server-side:
each projected occurrence carries a `baseEventId` pointing back to its source
event, so editing the source updates every future occurrence. A `dayOfMonth`
past the end of a month clamps to that month's last day. All recurrence math
uses the server's local timezone.

## API

All endpoints require authentication and operate only on the caller's own
events.

| Method   | Path                             | Purpose                                |
| -------- | -------------------------------- | -------------------------------------- |
| `GET`    | `/api/calendar/events?from=&to=` | Events in a range, recurrence expanded |
| `POST`   | `/api/calendar/events`           | Create an event                        |
| `PUT`    | `/api/calendar/events/:eventId`  | Update an event                        |
| `DELETE` | `/api/calendar/events/:eventId`  | Delete an event                        |

Range queries take epoch-millisecond `from`/`to` bounds and span at most 13
months per request. A user may store up to 2000 events; titles are limited to
200 characters and notes to 10,000.

---

# Automations

URL: https://docs.librewebui.org/automations

# Automations

Automations run an instruction on a schedule and deliver the result as a
normal chat session. A daily news digest, a weekly review, a monthly report:
each run executes headlessly on the server, lands in your chat list, and can
be opened and continued like any other conversation.

## Anatomy

An automation has a name, free-text instructions, one or more triggers, an
optional model (empty means Auto: your default chat model at run time), and a
notification preference (in-app or off). Names and instructions are encrypted
at rest. Every automation belongs to the user who created it.

Triggers reuse the calendar's shared model — `once`, `hourly`, `daily`,
`weekly`, `monthly`, `yearly` — and an automation may hold up to five. The
next run is always the earliest upcoming occurrence across its triggers,
computed in the server's local timezone.

## Execution

A scheduler tick runs every minute behind a coordination lease, so exactly
one replica advances schedules. When an automation is due, the tick records a
run, enqueues a durable `automation.run.v1` job, and advances `next_run_at`
with a compare-and-set so each occurrence fires at most once. The job creates
a chat session titled after the automation, then queues the instruction
through the same durable chat-generation pipeline every conversation uses —
provider routing, persona defaults, and persistence included.

If the server was down when an occurrence passed, the next tick fires that
occurrence once and skips any older missed slots. Pausing an automation
clears its schedule; resuming or editing recomputes it from now. Deleting an
automation removes its run history through a foreign-key cascade.

Runs settle from the durable job ledger: succeeded when the chat generation
finished, failed when either job dead-lettered, and failed as `stalled` when
a queued run never started within 30 minutes.

## API

All endpoints require authentication and operate only on the caller's own
automations.

| Method   | Path                                     | Purpose                       |
| -------- | ---------------------------------------- | ----------------------------- |
| `GET`    | `/api/automations`                       | List automations              |
| `POST`   | `/api/automations`                       | Create an automation          |
| `GET`    | `/api/automations/occurrences?from=&to=` | Upcoming computed occurrences |
| `GET`    | `/api/automations/runs`                  | Run history (filterable)      |
| `GET`    | `/api/automations/runs/summary`          | Unseen count + 30-day buckets |
| `POST`   | `/api/automations/runs/seen`             | Mark finished runs as seen    |
| `GET`    | `/api/automations/:automationId`         | Read one automation           |
| `PUT`    | `/api/automations/:automationId`         | Update an automation          |
| `DELETE` | `/api/automations/:automationId`         | Delete an automation          |
| `POST`   | `/api/automations/:automationId/pause`   | Pause the schedule            |
| `POST`   | `/api/automations/:automationId/resume`  | Resume the schedule           |
| `POST`   | `/api/automations/:automationId/run`     | Run now (202 with a run id)   |

A user may keep up to 50 automations; names are limited to 200 characters and
instructions to 20,000.
