TinyWebUI
TinyWebUI is a small, local-first chat workspace for any OpenAI-compatible model, with MCP tools. Chats, documents and tool output stay in one SQLite file on your machine. There is no hosted backend, no account to create, no build step and no frontend framework.
Bring an endpoint (OpenRouter, OpenAI, Groq, Ollama, vLLM, LM Studio, or anything else that speaks the OpenAI API), pick a model and a system prompt, and start working. It can grow from a personal workspace into a team deployment with sign-in, roles, per-user data and an admin panel, without changing anything else.
It comes with two TinySuite MCP servers already connected:
- TinySearch searches, reads and reranks the web locally, then gives the model only the evidence worth putting in its context. It doesn’t need a search API key.
- TinyContext is local long-term memory. It saves short memories in SQLite and recalls only what fits the token budget.
Highlights
Section titled “Highlights”- Durable conversations. Chats are stored in SQLite, with edit, retry, rewind, folders and full-text search. A running turn belongs to the server, not the browser tab, so you can reload or close the laptop and come back to it.
- Steer a turn while it runs. Press Enter while the model is working and your message reaches it at the next safe point. Press Alt+Enter to hold the message as a follow-up instead.
- Web search and memory included. TinySearch and TinyContext are connected from the first start.
- MCP over stdio, Streamable HTTP or SSE. Calls that can change something wait for your approval. Read-only calls in the same round run in parallel.
- Attachments. Text and source files, PDFs, DOCX and images. HEIC and AVIF photos are converted automatically.
- Hybrid local search. BM25 combined with a small on-device embedding model searches documents and past chats. Nothing leaves the machine.
- Long chats that stay affordable. Prompt prefixes are deterministic, so caches get reused. Large tool results are compacted once, and the full output stays one
expand_contextcall away. - You can see what you spend. Every round shows its token use, cache reads and cache writes.
- Automations. Cron-scheduled prompts that run in a chat of your choice. The model can create them too.
- Private by default. The first account becomes the administrator, and everyone after that needs a single-use invitation. Teams can put SSO in front instead.
- Managed from files. Every setting can come from version-controlled config, which hot-reloads, refuses typos and has a fingerprint you can check against a running instance. Keys stay out of it.
- Integrations. MCP servers that offer a management page, like TinySearch and TinyContext, get one in the app: each person sees and manages their own memories and search history.
- Themes. Fall Fairy and Cyber Grid are included, and a theme is just a folder with a CSS file.
Quick start
Section titled “Quick start”Requires Node.js 22.13 or later. The bundled TinySearch and TinyContext servers also need uv, which fetches them (and Python 3.12 if necessary) the first time they start.
npx tinywebuiOpen http://127.0.0.1:7777 and create the first account with an email address and a password of at least 10 characters. That account becomes the administrator and can invite others from the Admin panel. Settings then opens on its first step: add a connector (any OpenAI-compatible URL, with an API key if it needs one), then the models people can pick from it. Use --port and --host to change where it listens.
npx tinywebui keeps its config, keys, database and embedding models in one folder per user: ~/Library/Application Support/TinyWebUI on macOS, %LOCALAPPDATA%\TinyWebUI on Windows and ~/.local/share/tinywebui (or $XDG_DATA_HOME/tinywebui) on Linux. Run it in a folder that has its own tinywebui.config.json, or pass --config, to use that instead.
npx tinywebui@latest always runs the newest release. If you installed it with npm install -g tinywebui, update with npm install -g tinywebui@latest.
Or run from a clone of the repository (Node.js 22.18 or later, since the server runs its TypeScript directly):
npm installnpm startDocker
Section titled “Docker”docker compose builddocker compose up -dOpen the page and create the administrator in the browser. The Compose example publishes port 7777 on the host’s loopback interface only. Keep the /data volume: it holds the accounts, password hashes, conversations, config and keys.
The default image is Debian slim and has hybrid search, with the embedding model baked in at build time. Dockerfile.lexical builds a small Alpine image with keyword search only. Both keep data in a /data volume and have a /readyz health check. The images don’t include uv, so mount an mcp.json that connects to TinySearch and TinyContext over HTTP, or one that leaves them out. The deployment guide on GitHub walks through a full scripted deployment.
Development mode
Section titled “Development mode”npm run dev restarts the server when src/ or bin/ change. Files in public/ are served as-is, so a browser refresh is enough for them.
Configuration
Section titled “Configuration”tinywebui.config.json describes the deployment: endpoints, connectors, models and policy. Start from example.tinywebui.config.json, or let the Settings panel write it. Every model belongs to a connector, an OpenAI-compatible endpoint:
{ "connectors": [ { "id": "openrouter", "label": "OpenRouter", "baseUrl": "https://openrouter.ai/api/v1", "apiKey": "${OPENROUTER_API_KEY}", "models": [ { "id": "flash", "label": "DeepSeek Flash", "model": "deepseek/deepseek-v4.1-flash" } ] }, { "id": "local", "label": "Local models", "baseUrl": "http://localhost:11434/v1", "models": [{ "id": "llama", "label": "Llama 3.2", "model": "llama3.2" }] } ], "model": "flash", "maxToolRounds": 12}A local runtime (Ollama, vLLM, LM Studio, llama.cpp) needs no apiKey. Older flat files with a top-level baseUrl, apiKey and models still work.
The Settings panel opens on a hub of connectors with their models. Each model has its own page for its prompt, sampling, tool budget, caching and request body, and blank fields inherit the defaults below. Every change is written back to the config file. API keys are write-only: the browser learns that one is set and its last four characters, never the value, and leaving the field blank keeps the stored key.
Environment variables
Section titled “Environment variables”| Variable | Effect |
|---|---|
TINYWEBUI_CONFIG |
Path to the config file (.json, or .js/.mjs, see Deployment as code) |
TINYWEBUI_MCP |
Path to mcp.json (default: next to the config file) |
TINYWEBUI_DB |
Path to the SQLite database (default: tinywebui.db next to the config file) |
TINYWEBUI_SECRETS |
Path to the secrets file (default: tinywebui.secrets.json next to the config file) |
TINYWEBUI_API_KEY, OPENROUTER_API_KEY |
API key for models that name no connector; overrides the file |
TINYWEBUI_BASE_URL, TINYWEBUI_MODEL |
Endpoint and model; override the file |
TINYWEBUI_RETRIEVAL_MODE |
auto, lexical, dense or hybrid (see Search) |
TINYWEBUI_MODELS_DIR |
Where embedding models are stored (default: models/ next to the config file) |
TINYWEBUI_SESSION_SECRET |
Signs session cookies. Without it, one is generated and kept in the secrets file |
TINYWEBUI_LOG_LEVEL |
debug, info (default), warn, error or silent. Audit events are always written |
Settings reference
Section titled “Settings reference”| Setting | Default | What it does |
|---|---|---|
maxToolRounds |
12 |
Tool-use budget for one user message. A model response that calls one or more tools uses one round. |
askUserTimeoutSeconds |
120 |
How long an ask_user question waits before the run continues on the model’s own assumptions. 0 waits until the question is answered or the run is stopped. Automation runs never wait. |
toolApproval |
"writes" |
Which tool calls need your approval: "writes", "all" or "off". See Tool approval. |
cache |
true |
Prompt-cache shaping where the provider supports it. |
cacheMode |
"auto" |
auto, implicit (stable prefix only), explicit (Anthropic cache_control), rolling (OpenRouter) or off. |
cacheTtl |
"5m" |
Anthropic cache lifetime. "1h" costs more to write and suits chats with long pauses between turns. |
reasoningReplay |
"auto" |
Which reasoning earlier assistant turns send back: reasoning_details (OpenRouter, Anthropic-style), reasoning_content on tool-call turns (direct DeepSeek), or omit. Set it for a gateway that auto doesn’t recognise. |
compactThreshold |
60000 |
History tokens (system prompt and tool definitions excluded) at which older tool results are compacted. 0 disables it. |
keepTurns |
2 |
How many recent user turns keep their tool results in full. |
compactMinSaved |
20000 |
The minimum number of characters a compaction must remove to be worth the cache miss it causes. |
maxInlineChars |
30000 |
Tool results larger than this are stubbed for later turns as soon as they arrive. |
maxTurnChars |
120000 |
Tool results larger than this are stubbed even within the turn that fetched them. |
maxHistoryTokens |
100000 |
Hard window. Beyond it, the oldest turns stop being sent to the model, but they stay in the transcript. 0 disables it. |
llmCompaction |
true |
Before the window drops turns, the chat’s model summarises them. This costs one extra request each time the window moves. If the summary fails, the turns are simply dropped. |
compactionMaxTokens |
8192 |
Output limit for that summary. Reasoning models think within this limit, and a cut-off summary is rejected, so leave headroom. |
expandCharBudget |
8000 |
The most one expand_context call can return. |
timezone |
"" |
IANA zone used for today’s date and for automations. Empty uses the server’s zone. |
extraBody |
{} |
Merged into every completion request. Gateway-specific options go here. |
dbPath |
"" |
Database location. Empty means tinywebui.db next to the config file. |
The model is told its full tool budget and today’s date. The last tool result of each round ends with a note of the rounds left, and that note becomes a warning for the final two rounds. When the budget is spent, TinyWebUI makes one last request with tools disabled, so the model answers from the evidence it already has.
The full schema, including the access and retrieval settings, is in docs/config.schema.json. You can also print it with tinywebui schema.
Model catalog
Section titled “Model catalog”The models listed under the connectors are the catalog: only they can be picked, and each can be named and tuned. With no models listed anywhere, any model id works.
{ "model": "quick", "connectors": [ { "id": "openrouter", "baseUrl": "https://openrouter.ai/api/v1", "apiKey": "${OPENROUTER_API_KEY}", "models": [ { "id": "quick", "label": "Quick", "description": "Everyday questions", "model": "deepseek/deepseek-v4.1-flash", "temperature": 0.3 }, { "id": "deep", "label": "Thorough", "model": "anthropic/claude-sonnet-5", "systemPrompt": "You are a careful research assistant...", "maxToolRounds": 20, "extraBody": { "reasoning": { "effort": "high" } } } ] } ], "access": { "roles": { "user": { "models": ["quick"] } } }}idis the name everything else uses: themodelsetting, role model lists and each person’s saved choice.modelis the provider’s id; if it is left out, theidis sent.- People only see
labelanddescription. They can’t type in an id, and the provider id never reaches the browser. - Per-model overrides. An entry can set
systemPrompt,temperature,maxTokens,maxToolRounds,cacheMode,cacheTtl,reasoningReplayandextraBody. These replace the global values for turns with that model, exceptextraBody, which is merged with the global one. "enabled": falsehides an entry but keeps its settings. Anyone who had picked it falls back to the default model. The default must be an enabled entry.- Connectors (
{ "id", "label", "baseUrl", "apiKey", "models" }) let one catalog mix endpoints. A flat top-levelmodelslist still works: there, an entry’sconnectornames its endpoint, and without one it uses the top-levelbaseUrlandapiKey. tags(vision,reasoning,tools) say what a model can do. An untagged model is treated as unknown and allowed everything; a tagged model withoutvisionrefuses image attachments in the page.- Editable from the UI by an admin (or the one person in local mode), in Settings, with the same checks as the file. Startup, reload and every save refuse a catalog with an unknown key, a duplicate id, or a reference to an entry that doesn’t exist.
MCP tools
Section titled “MCP tools”MCP servers are defined in mcp.json next to the config file, in the same shape that MCP desktop clients use.
Pre-loaded: TinySearch and TinyContext
Section titled “Pre-loaded: TinySearch and TinyContext”Until an mcp.json exists, TinyWebUI starts with these two TinySuite servers, launched by uv, with their management pages on:
{ "mcpServers": { "tinysearch": { "command": "uvx", "args": ["--python", "3.12", "--from", "tinysuite-search[server]>=0.6.7", "tinysearch"], "env": { "TINYSEARCH_MANAGEMENT": "on" } }, "tinycontext": { "command": "uvx", "args": ["--python", "3.12", "--from", "tinysuite-context[server]>=0.5.2", "tinycontext"], "env": { "TINYCONTEXT_MANAGEMENT": "on" } } }}uv picks up new releases of both servers by itself when they start. The version floors make sure nobody runs a release older than the one TinyWebUI was tested with. TinySearch still records no activity unless you turn that on too.
With local accounts or a sign-in gateway, every user shares these two stdio servers, including TinyContext’s memories. To keep each person’s memories apart, run the servers over HTTP as shared servers with an identity.
| Server | What the model gets |
|---|---|
| TinySearch | Web search, clean Markdown from pages, and a browser for pages that need clicking. It runs locally and needs no search API key. |
| TinyContext | Memory across chats: it saves short memories and recalls the relevant ones within a token budget. Stored locally in SQLite. |
The first start takes a little longer while uv fetches the packages. TinyContext also downloads its embedding model once.
Without uv, TinyWebUI still starts and everything else works. The two servers are switched off, and it tells you why. On the command line, startup ends with tinycontext, tinysearch off until uv is installed, and npx tinywebui doctor reports both as failed for the same reason. In the app, both show needs uv in the tools menu and the MCP panel.
Install uv and restart TinyWebUI; no other setup is needed. The restart matters because the uv installer adds itself to your PATH only for programs started after it.
Saving the MCP panel writes these servers to mcp.json. After that the file is in charge: edit it to change them, or remove them to leave them out. An empty { "mcpServers": {} } means no servers at all.
Adding servers
Section titled “Adding servers”The repository’s example.mcp.json has the two TinySuite servers plus a disabled HTTP example. Copy it to start, then add servers like these:
cp example.mcp.json mcp.json{ "mcpServers": { "files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] }, "research": { "url": "http://127.0.0.1:8000/mcp", "headers": { "Authorization": "Bearer ..." } } }}- Stdio servers take
command,args,envandcwd. - Remote servers take
url, optionalheaders, and"transport": "sse"for SSE. The default transport is Streamable HTTP. "disabled": truekeeps a server defined but disconnected.
The MCP panel validates and saves mcp.json, then reconnects without a restart. You can switch off single tools without disconnecting their server. The model sees tools under stable server__tool names.
Tool approval
Section titled “Tool approval”A tool call that can change something waits for you. It appears in the transcript with its arguments and three choices: allow, always allow this tool and deny.
- A tool counts as read-only only when its server marks it with the MCP
readOnlyHintannotation. Any tool without that mark is treated as one that can write. toolApprovalsets the policy:"writes"(default) asks before any call that isn’t read-only,"all"asks before every call and"off"never asks. The tools panel can override it for a single tool (confirmTools/autoApproveTools).- TinyWebUI’s built-in tools never ask, except
manage_mcp, which asks before every change, even whentoolApprovalis"off". - Scheduled automation runs have no one to ask, so they refuse a call that would need approval and report it.
Calls from one round that are marked both readOnlyHint and idempotentHint run in parallel. Other calls run one at a time. Either way, the transcript and the results come out in the order the model asked for them.
Built-in tools
Section titled “Built-in tools”| Tool | What it does |
|---|---|
read_document |
Searches or reads a document attached to the chat. |
search_chats |
Searches your earlier conversations (never the current one) and returns the questions with the answers they got. |
expand_context |
Searches or pages through the full output behind a compacted tool result. |
ask_user |
Asks you a question mid-turn, optionally with choices, and waits for the answer. |
manage_tasks |
Keeps a checklist for the current chat, shown in the right rail. |
manage_automation |
Creates, edits and runs scheduled automations. |
manage_mcp |
Adds, changes, removes, enables and disables the servers in mcp.json. See Letting the model edit MCP servers. |
Letting the model edit MCP servers
Section titled “Letting the model edit MCP servers”Ask in a chat (“connect the GitHub MCP server”) and the model can edit mcp.json with manage_mcp. A local server is a program that runs on your machine, so every change waits for your approval, in every approval mode. A scheduled run can’t use it. Only roles with the mcp feature get it, and it refuses to change servers that are set in code or frozen.
mcp.json holds credentials, and the model can write them but never read them back. Its results show the names of headers and environment variables, never their values, and an update to headers or env changes only the entries it names, so a key is never repeated to change something beside it.
To keep a key out of the file and out of the chat entirely, set it in TinyWebUI’s environment and refer to it as ${NAME}:
{ "mcpServers": { "github": { "url": "https://api.githubcopilot.com/mcp/", "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" } } }}${NAME} works in url, headers, args and env values, and a variable that isn’t set stops that server from connecting, with its name in the error. A key you paste into the chat is stored in the chat like any other message, and the model’s own call that writes it is kept in the transcript, so prefer ${NAME} for real keys.
Changes are saved straight away and the servers reconnect when the reply finishes, so a new server’s tools are available from your next message.
Integrations
The Integrations panel lists every connected MCP server. A server that offers a management page gets a Manage view there: tables and statistics about what it holds, and buttons that run its own tools. TinyContext shows each person their own memories, with search, editing and deletion; TinySearch shows their own search history when its activity history is on. Admins also see the servers’ instance-wide views, such as TinySearch’s domains and health, when the server is given a management token.
Pages are built from plain data that TinyWebUI draws with its own components; a server never sends HTML or script. Any MCP server can offer one by implementing the tinysuite.manage/1 contract. The integrations feature controls who sees the panel.
Shared servers for many users
Section titled “Shared servers for many users”One TinySearch and one TinyContext can serve every user of a multi-user deployment. Give a remote server in mcp.json an identity, and TinyWebUI tells it on every call which signed-in user the call is for:
{ "mcpServers": { "tinycontext": { "url": "http://tinycontext:8000/mcp", "identity": { "header": "X-TinyContext-User-Id" } }, "tinysearch": { "url": "http://tinysearch:8000/mcp", "identity": { "header": "X-TinySearch-User-Id" } } }}- The identity comes from the owner of the chat the turn runs in, never from the browser, the model’s arguments or the MCP server. Automations run as the owner of their chat.
- Each user gets their own MCP connection to the server, so server-side session state, such as TinySearch’s browser, is never shared. A call for a user who doesn’t exist or isn’t approved is refused, never sent as someone else.
claimchooses what the header carries: TinyWebUI’s own user id (id, the default), the gateway’s id (externalId) or the email (email).identityworks only onurlservers. A stdio server is one process that every user shares, with whatever data it keeps.
The server must believe that header only from TinyWebUI’s address: TinyContext’s hosted mode does this. Keep the MCP servers on a private network that only TinyWebUI can reach. The shared MCP guide on GitHub walks through a deployment and a single-machine trial.
Chats and attachments
Section titled “Chats and attachments”Conversations are stored in tinywebui.db. You can edit and resend a message, retry an answer, rewind, file chats into folders and search them all. The search covers your messages and the final answers, but not reasoning, tool calls or raw tool output, so the results stay relevant. An outline on the right edge lists the questions in a long chat, and clicking one jumps to it.
Turns run on the server. Reloading or disconnecting doesn’t cancel a turn: reopen the chat to rejoin its stream, or press stop. While a turn is running, press Enter to steer it, or Alt+Enter to hold a follow-up that you send when you’re ready.
Documents. Attach files with the paperclip, by dragging them onto the composer, or by pasting a long block of text. Text and source files, CSV, JSON, PDFs with extractable text and DOCX files are stored as documents for the chat. The model reads the relevant parts with read_document instead of getting the whole file in its context. Each document can be up to 5 MiB. Files you attached before can be reattached from the library without uploading them again.
Images are sent to the model with the message: up to eight per message, each up to 5 MiB. JPEG, PNG, WebP and GIF are sent as they are, and HEIC and AVIF are converted to JPEG. This needs a model and endpoint that accept OpenAI-style image content.
Not supported: OCR (scanned or image-only PDFs are refused with a clear error) and legacy .doc files (save them as .docx or .pdf).
Search
Section titled “Search”read_document and search_chats share one retrieval engine. A chat turn is indexed as the question together with the answer it finally got, without the narration or tool calls in between.
Search is hybrid by default. SQLite full-text search (BM25) matches exact words, and a small local embedding model matches meaning, so a question about an “automobile” finds the paragraph about the “sedan”. The two rankings are combined with Reciprocal Rank Fusion, using the same ONNX models and fusion as the TinySuite siblings TinySearch and TinyContext. Everything runs locally.
- Install.
npm installbrings the embedding runtime (onnxruntime-nodeand@huggingface/tokenizers, both optional dependencies). The firstnpm startdownloads thefastmodel throughtinywebui models ensure. The Docker image includes it. - Fallback. If the model or the runtime is missing (an offline first start, a platform where onnxruntime can’t install, or
npm install --omit=optional), the defaultautomode runs BM25 only and logs why. - Keyword search only. To skip hybrid search and the download, run
TINYWEBUI_RETRIEVAL_MODE=lexical npm startor set{ "retrieval": { "mode": "lexical" } }. - Pinning the model.
{ "retrieval": { "mode": "hybrid", "model": "fast", "modelSha256": "<sha256 from tinywebui models pull>" } }refuses to start with any other model file. - No downloads at runtime. Models only arrive through
tinywebui models pull/models ensureor the Docker build. ONNX Runtime telemetry is switched off.
How retrieval works, and how to tune it
Section titled “How retrieval works, and how to tune it”Small-to-big retrieval. Each document is split twice:
| Passage | Chunk | |
|---|---|---|
| What it is | What read_document returns to the model |
What gets embedded |
| Size | passageSize characters (default 1,800, about 400 tokens) |
The embedding model’s token limit: 256 for fast, 512 for the others |
| Overlap | passageOverlap characters (default 200) |
chunkOverlap tokens (default 32) |
| Used for | BM25 (every mode) and the text the model reads | Dense matching (dense, hybrid) |
The question is compared with every small chunk, each passage takes its best chunk’s score, and the model gets whole passages. Small chunks keep the match precise, and large passages give the model enough context to answer. Chunk size follows the model, so every part of a passage is embedded, even one longer than the model can read at once.
Modes.
auto(default):hybridwhen the model is installed,lexicalwhen it isn’t.lexical: BM25 only.dense: embeddings only.hybrid: both, fused.
An explicit dense or hybrid refuses to start without the model instead of falling back. A checksum that doesn’t match modelSha256 stops startup in every mode. tinywebui models verify and tinywebui doctor show which mode will run and why.
Models.
| Preset | Model | Notes |
|---|---|---|
fast (default) |
all-MiniLM-L6-v2 | English, about 90 MB |
balanced |
bge-small-en-v1.5 | English |
quality |
bge-base-en-v1.5 | English |
multilingual |
granite-embedding-107m-multilingual (Apache-2.0) | About as fast as fast, 430 MB download |
For questions and documents in different languages, keyword matches rarely help, so dense mode or a higher denseWeight usually ranks better. A custom bundle works with modelDir.
Embeddings are computed once. Document chunks are embedded when the document is attached, and chat turns when each run ends. Older chats and documents are embedded in the background at startup. Vectors are stored in SQLite, and a query only embeds the question. Switching models re-embeds everything automatically. A turn whose answer changes is re-embedded, and deleting or rewinding removes its vectors.
Tuning:
passageSizeandpassageOverlapset what the model gets back. Bigger passages give more context per hit, but fewer hits fit in one result.chunkOverlapsets how much neighbouring chunks share.denseWeight(default 0.5) andrrfK(default 60) control the fusion.queryPrefixanddocumentPrefixare for models that expect instructions. For bge, setqueryPrefixto"Represent this sentence for searching relevant passages: ".
Changing these is safe: stored documents are re-split when the passage settings change, and re-embedded when the model or chunkOverlap changes.
Adding a corpus. The engine in src/retrieval/retrieval.js can search any source that describes its units through a small adapter (units, candidates, lexical, hydrate…). Register it with retrieval.register(name, corpus) and it gets lexical, dense and hybrid search, storage and backfill.
Automations
Section titled “Automations”An automation posts its instructions into a chat you choose, on a five-field cron schedule in an IANA timezone. You can create automations in the automations view, or ask the model to manage them with manage_automation.
- Runs use the current model and enabled tools, and appear both in the chat and in the automation’s run history.
- Run now triggers one immediately. It waits for the chat’s current turn to finish.
- Automations only run while TinyWebUI is running. A run that was missed, or that falls while the chat is busy, is skipped.
- No external notifications are sent.
Long chats: caching and compaction
Section titled “Long chats: caching and compaction”Tool results can be much larger than the conversation around them. TinyWebUI keeps every result in full in SQLite, but avoids sending it again and again:
- Cache-friendly prefixes. Between compactions, history is append-only and serialized deterministically, so providers can reuse the matching prompt prefix.
- One-time compaction. When history crosses
compactThreshold, older tool results are replaced, once, with compact stubs. A stub holds an artifact ID, a hint about the structure, and the first and last lines verbatim. Older images are replaced with a short note. - Nothing is lost. The model can get the exact content back through
expand_context. - A moving window as a last resort. If history is still over
maxHistoryTokens, which usually happens in a very long chat with few tool calls, the oldest turns stop being sent. The cut falls on a user message and leaves a note that earlier conversation was omitted. WithllmCompaction, those turns are first summarised (together with any earlier summary) into the system prompt. Rewinding to a point before the cut removes the summary.
Provider specifics. On OpenRouter, TinyWebUI sends a stable per-chat session_id so sticky routing keeps caches warm, and Claude gets the automatic rolling cache directive. Local and other endpoints get no OpenRouter-only fields. Set cacheMode only if your gateway needs a different cache dialect, or set cache: false to turn cache shaping off.
Usage statistics
Section titled “Usage statistics”Every round in the transcript shows its token use, including cache reads and writes when the provider reports them. The Statistics view totals usage over time and by model, and can drill down from all time to a single day. Extra summary requests made by llmCompaction show up there too.
Token attribution. Each assistant request stores a counts-only breakdown of where its tokens went. The input side covers the operator prompt, harness, MCP guidance, tool schemas per server, conversation, reasoning sent back, tool history and results, and images. The output side covers reasoning, tool calls, intermediate text and the final answer.
- The provider’s totals and reported reasoning counts are authoritative. Everything else is estimated at about four characters per token, with images counted as 1,500 tokens, and is marked
~. - The gap to the provider’s total is shown as a signed difference and never rescaled away.
- Cache reads are part of the input, not an extra component.
- Requests made before attribution existed are not reconstructed.
Access
Section titled “Access”Choose how people sign in with authMode:
| Mode | authMode |
For | Sign-in |
|---|---|---|---|
| Local accounts | "multiuser" (default) |
You, or a group, on your own machine or server | Email and password. The first account is the administrator and invites the rest. |
| Local only | "local" |
You, on this machine | None |
| Sign-in gateway | "trusted-header" |
A team or organization | Google, Apple, Microsoft, any SSO, through a sign-in gateway |
Local accounts
Section titled “Local accounts”This is the default for npx tinywebui, npm start and Docker.
- The first account is the administrator. Create it in the browser with an email address and a password of at least 10 characters. The email is only checked for format: TinyWebUI sends no mail and needs no mail server.
- Everyone else needs an invitation. An admin creates a single-use invitation link in the Admin panel, optionally for one email address. It expires after 7 days by default (anything from 1 hour to 30 days).
- Sessions use HTTP-only signed cookies and last
sessionTtlDays(default 30). - Lockout. After five wrong passwords, that address is locked out for 15 minutes.
- HTTPS. Serve it over HTTPS, for example behind a reverse proxy, whenever it is reachable from outside your machine.
Local only
Section titled “Local only”"authMode": "local" turns off sign-in. There is one person, who has full access to every chat, setting and tool, and no account management. This mode only listens on loopback: it refuses to start with any other --host, and it answers only loopback host names, plus any in allowedHosts, which guards against DNS rebinding.
Sign-in gateway
Section titled “Sign-in gateway”TinyWebUI sits behind a sign-in gateway that handles the login, such as Cloudflare Access, oauth2-proxy, Authelia or Authentik. Any identity provider the gateway supports works. The gateway verifies each person and passes their identity in request headers:
{ "authMode": "trusted-header", "trustedProxyCidrs": ["172.20.0.0/16"], "trustedUserIdHeader": "x-tinysuite-user-id", "trustedEmailHeader": "x-tinysuite-email", "trustedNameHeader": "x-tinysuite-name", "trustedRoleHeader": "x-tinysuite-role", "logoutUrl": "https://<team>.cloudflareaccess.com/cdn-cgi/access/logout", "access": { "bootstrapAdmins": ["<your gateway user id>"], "newUsers": "approved" }}- Accounts create themselves on a person’s first request. They are keyed on the gateway’s stable user ID, not on email, so a changed address keeps the same account.
- The first admin comes from the config. Anyone listed in
access.bootstrapAdminsis always an approved admin. The gateway can also sendadminoruserin the role header, but a decision recorded in the files outranks it.
Users, roles and admins
Section titled “Users, roles and admins”These apply to both local accounts and a sign-in gateway.
- Data is private to each user. This covers chats, documents, folders, search, automations and usage. The server refuses any request for another user’s data by ID, and a test matrix tries every route against another user’s IDs.
- Roles grant features. For example, users can be limited to chatting while admins set up models and MCP servers. A browser can never promote itself.
- Admins get an Admin item under Statistics, where they can:
- invite people (local accounts);
- approve pending users (set
access.newUsers: "pending"to require approval); - disable an account, which returns 403 immediately and stops that user’s automations;
- change roles;
- read any user’s chats, documents and live turns. This view is read-only, and each view is recorded in the audit log.
- Settings belong to the operator. Without the
settingsfeature, the model, system prompt and approval rules are read-only, and credentials and the gateway address are hidden. Endpoints, keys and the model catalog can only be changed by an admin. Withoutmcp, the MCP servers are hidden. Each person picks their own model from their role’s allowed list, and can pick a theme (access.customizesets what people may personalise:theme,instructions). - Audit log. New accounts, invitations, rejected requests, role and status changes, config changes and admin views are written to stdout as
[tinywebui:audit]JSON lines. Messages and secrets are never logged.
Deployment as code
Section titled “Deployment as code”Every part of a deployment, down to what each role can see, can be set up from files. An agent or a script can build it, commit it to git and hand it over, and the running app stays in step with the files.
Configuring from JavaScript
Section titled “Configuring from JavaScript”start() takes the same settings, so you can run an instance without any files:
import { start } from 'tinywebui';
const server = await start({ port: 7777, config: { apiKey: process.env.OPENROUTER_API_KEY, model: 'anthropic/claude-sonnet-5' }, mcpServers: { files: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] } }, configFile: false, // or a path; false reads and writes no config file dbPath: ':memory:',});// later: await server.shutdown();The tinywebui command does the same when it finds tinywebui.config.js (or .mjs) in the working directory, or when TINYWEBUI_CONFIG points at one. The file’s default export is the options object, or a function (async is fine) that returns it. --port and --host still override it.
Layers apply lowest first: built-in defaults, the JSON config file, environment variables, then config from JavaScript. Keys set in JavaScript are locked: the Settings panel shows them read-only and the API refuses to change them. tinywebui.config.json is still read and written for everything else, unless the options set configFile: false. In that case changes last only as long as the process. Passing mcpServers replaces mcp.json and makes the MCP panel read-only.
Three kinds of setting
Section titled “Three kinds of setting”| Kind | Where it lives | Can the UI change it? |
|---|---|---|
File-only: authMode, sessionSecret, sessionTtlDays, trusted*, logoutUrl, allowedOrigins, allowedHosts, dbPath, retrieval, autoMigrate, frozen, access (admins edit the user role’s features and models, new-user policy and customize through the Admin panel) |
tinywebui.config.js or tinywebui.config.json |
Never. The API refuses, even for admins. |
Locked: anything set in tinywebui.config.js, including mcpServers |
tinywebui.config.js |
No. Shown read-only. |
| Editable: everything else | tinywebui.config.json, mcp.json |
Yes, and every change is written back to the file. |
- Freeze everything with
frozen: truein either file. Settings, tools and MCP servers then can’t be changed from the UI or the API. You change the files and let them reload instead. User data and admins’ decisions about users still work normally. - Mistakes fail loudly. An unknown key (usually a typo), a value of the wrong type or a value outside its allowed set stops startup, reload, a UI save and
tinywebui validate, and every problem is named. Nothing silently falls back to a default. - Admin decisions live in the files. Approving, disabling or changing someone’s role writes
access.usersintinywebui.config.jsonbefore the database is touched. The files outrank the database: delete the database and restart, and every approval and ban is still in force. - Edits apply without a restart. Changes to
tinywebui.config.jsonormcp.jsonare picked up automatically, or onkill -HUP. A reload is all or nothing: a file that doesn’t parse or validate is rejected and logged, and the running config stays as it was. Changes totinywebui.config.jsorauthModeneed a restart. - Fingerprints. The effective config has a hash that never includes secrets. It is printed at startup, shown in the Admin panel, returned by
/readyzand included in config-related audit lines before and after each change. Runningtinywebui fingerprinton the committed files gives the same value, so you can check that a running instance matches a given commit.
A complete sign-in gateway setup
Section titled “A complete sign-in gateway setup”// tinywebui.config.js: committed to git, holds no secretsexport default { config: { authMode: 'trusted-header', trustedProxyCidrs: ['172.20.0.0/16'], baseUrl: 'http://gateway:8080/v1', // your model gateway; users never see it apiKey: process.env.MODEL_GATEWAY_KEY, model: 'fast', systemPrompt: 'You are the team assistant…', // locked: set here, so read-only in the UI logoutUrl: 'https://team.cloudflareaccess.com/cdn-cgi/access/logout', access: { bootstrapAdmins: ['google-oauth2|1234567890'], newUsers: 'pending', roles: { user: { features: ['chat', 'attachments', 'images', 'search', 'folders', 'model-picker'], models: ['fast', 'smart'] }, admin: { features: '*' } } } }, mcpServers: { // locked: users can use these tools, nobody can edit them search: { url: 'http://tinysearch:8000/mcp' }, context: { url: 'http://gateway:8080/context/mcp' } }};The deployment guide on GitHub takes this through a scripted container deployment: validate, migrate, doctor, start and verify the fingerprint.
Features
Section titled “Features”A role’s features list can include any of these. Each one covers both the UI and the API routes behind it: the UI hides what a role doesn’t have, and the server refuses it.
| Feature | Grants |
|---|---|
chat |
Chatting |
attachments |
Attaching documents |
images |
Attaching images |
search |
Searching chats |
folders |
Organizing chats into folders |
automations |
Scheduled automations |
statistics |
The usage view |
model-picker |
Choosing a model from the role’s list |
tools |
Turning tools on and off |
settings |
Changing settings |
mcp |
Managing MCP servers |
integrations |
The Integrations panel: your own data in servers that offer a management page, and for admins the servers’ own views |
admin |
Managing users |
oversight |
Reading other users’ chats |
Defaults. With accounts (multiuser or trusted-header), users get everything except tools, settings, mcp, admin and oversight, and admins get *. In local mode you get everything except user management.
Output on stdout is JSON or a single line, and logs go to stderr, so scripts can parse the output. Run any command with npx tinywebui <command>.
| Command | What it does |
|---|---|
tinywebui [start] [--port] [--host] [--config] |
Run the server |
tinywebui validate [path] |
Prints ok <fingerprint>, or lists every problem and exits with code 1 |
tinywebui effective [--role user|admin] |
What is in effect, how each setting can change, and what a role can see and do (also tinywebui config show) |
tinywebui fingerprint |
Hash of the effective config (never includes secrets) |
tinywebui schema |
JSON Schema for the config (also in docs/config.schema.json) |
tinywebui migrate [--check] |
Run schema migrations as an explicit step. --check exits with code 1 if the database is behind. Set autoMigrate: false to make this mandatory. |
tinywebui doctor |
Checks Node, config, database, model endpoint and each MCP server |
tinywebui users list |
Users, and the decisions pinned in the files |
tinywebui users set <id> [--role] [--status] [--clear] |
Record a decision in the config. A running server reloads it. |
tinywebui models pull <preset> [--dir] |
Download an embedding model (fast, balanced, quality, multilingual) |
tinywebui models ensure |
Download the configured model only if it is missing |
tinywebui models verify |
Load the configured model the same way startup does |
For orchestrators, GET /healthz reports that the process is answering, and GET /readyz reports that it can serve requests ({"ready":true,"version":"…","fingerprint":"…"}).
Security and limits
Section titled “Security and limits”- Network exposure.
localmode only ever listens on loopback. Use local accounts or a sign-in gateway, over HTTPS, before exposing TinyWebUI to a network. - MCP servers run with whatever access you give them. Treat each configured server and its credentials as trusted infrastructure, and use tool approval for anything that writes. A stdio server is shared by every user; use shared servers with an identity to keep users’ data apart.
- Model output is untrusted. Model-written text reaches the page only as text or through a Markdown renderer that escapes first, and the server sends strict security headers.
- Deleting a chat also deletes its documents, tool output and search vectors.
- Not included: OCR, cloud sync and branching chat history.
Development
Section titled “Development”npm test # type check, then the whole suite, about 10 secondsnpm run dev # restart on changes in src/ and bin/The server is TypeScript that Node runs directly, with no build step, so working from a clone needs Node.js 22.18 or later. The npm package ships JavaScript compiled from it and works from 22.13.
The tests cover message serialization, caching, compaction, the tool loop and approvals, retries, editing, search, streaming and rejoining, settings, password and gateway sign-in, config write-back and reload, and a tenant-isolation matrix. They also enforce the architecture: import directions, the harness boundary, and the stream event registry. AGENTS.md maps the codebase and says where new code goes.
Evals (npm run eval) run the tasks in evals/tasks/ against the configured model and score its behaviour: whether it uses tools instead of guessing, ignores instructions planted in tool output, and reports failures honestly. They make real API calls to your configured endpoint, so they cost money.
License
Section titled “License”TinyWebUI is licensed under the GNU Affero General Public License v3.0 or later. If you run a modified version for users over a network, you must offer them the corresponding source code for that version.