Architecture
How a request moves through Hubzoid, from chat surfaces and the edge to the bridge, agent runtimes, models, the workflow engine, databases and the Console.
Hubzoid is a small set of processes around one idea: every surface reaches the same Hub through a single bridge per Hub. The bridge builds the agent from the Hub folder, enforces identity and access, runs scheduled work, serves the Console and the hosted MCP endpoint, and streams answers back in the OpenAI chat format.
Two seams keep the parts that change most often, the surfaces and the model, replaceable without touching the Hub folder.
Surfaces
Where people and applications send messages.
- Web chat
- Slack
- Telegram
- Your application
Seam one: every surface calls the same OpenAI-compatible chat API
Bridge
/v1/chat/completionsOne process per Hub, on loopback. It identifies the caller, runs the Hub agent, guards restricted tools and streams the answer. It also serves the Console,
/mcpand scheduled work.Seam two: the runtime is chosen from
MODELRuntime
Hub files, tools and skills load the same way on all three.
- OpenAI Agents SDK
- Claude Agent SDK
- Local Codex
Model calls
Model
Through LiteLLM, the
claudeCLI forclaude-local, or thecodexCLI forcodex-local.- OpenAI
- Anthropic
- Azure OpenAI
- OpenRouter
- claude-local
- codex-local
/mcp and bring their own model.A chat request, end to end
- Surface
- Edge on :3080
- Open WebUI
- Bridge /v1
- Agent runtime
- Model provider
- The browser reaches the edge on the public port (
PORT, default 3080). The edge passes the chat app's pages and websockets to Open WebUI on a loopback port. - Open WebUI signs the person in (when sign-in is on), stores the conversation, and calls the bridge's
POST /v1/chat/completionswith the bridge key and the person's email in forwarded headers. - The bridge derives a chat id and the caller's identity (email, groups, surface). If Hubzoid manages access for this Hub, it requires the
use_hubgrant before anything runs. It saves any attached files to the chat's upload folder and flattens the message list into one prompt. - The runtime runs the Hub's agent: system prompt, tools, skills, knowledge and MCP connectors. Restricted tools pass through the access guard, which records every allow or deny decision before a call runs.
- The model provider answers through LiteLLM, through the
claudeCLI forclaude-local, or through thecodexCLI forcodex-local. - The bridge streams the answer back as server-sent events and records a usage row with tokens, estimated cost and duration, never message content.
A turn that calls a tool
The model never runs a tool itself. It asks, and the bridge checks access, runs the tool and hands back the result, as often as the answer needs. Here is one web chat turn on a Hub with a restricted ledger_lookup tool.
- 1Web chat
A person asks
Is bill 1042 a duplicate? Open WebUI sends the conversation to the bridge.
- 2Bridge
Who is asking
Reads the forwarded identity, checks Use this agent when access is managed in the Console, and turns the chat into one prompt.
- 3Runtime
The agent takes the turn
The Hub agent, built from the folder at start, hands the prompt to the model with its instructions, menus and tools.
- 4Model
Asks for a tool
Requests
ledger_lookupwith the bill number. The model only asks. It never runs code. - 5Bridge
Access check
The tool comes from
restricted/ledger.py, so the guard checks theledgerpermission and records the decision before anything runs. - 6Bridge
The tool runs
The function runs in the bridge process with the credentials of the Hub, and its result goes back to the model.
Steps 4 to 6 repeat for each tool the model asks for, such as
read_knowledgefor the matching rules. - 7Bridge
The answer streams back
The model writes the answer. The bridge streams it to the web chat and records usage, never message content.
Processes and ports
hubzoid run my-hub starts these processes. Only the edge port is meant to be reachable from other machines.
| Process | Started as | Listens on | Role |
|---|---|---|---|
| Bridge | uvicorn hubzoid.server:build_app | 127.0.0.1:8000 (BRIDGE_PORT, --bridge-port) | Agent runtime, access guard, workflow engine, Console and artifact viewer, hosted MCP. Always loopback. |
| Open WebUI | open-webui serve | 127.0.0.1, port PORT plus 40000 (43080 by default, HUBZOID_OWUI_PORT to override) | Web chat, accounts, sessions, chat history, per-user API keys. |
| Edge | uvicorn hubzoid.edge:_factory | HUBZOID_HOST:PORT, default 127.0.0.1:3080 | The single public front door. Routes paths to the bridge or Open WebUI. |
| Slack adapter | hubzoid slack run (with --slack) | No port. Slack Socket Mode connects outward. | Relays Slack messages to the bridge. |
| Inbound server | hubzoid inbound run (with --whatsapp, --telegram or --webhook) | 127.0.0.1:8100 (HUBZOID_INBOUND_PORT) | Verifies and handles WhatsApp, Telegram and generic webhook deliveries. |
| Shared browser | playwright-mcp, started by the bridge when HUBZOID_BROWSER=true | Port 8931 (HUBZOID_BROWSER_PORT) | One resource-limited browser shared by every agent as MCP tools. |
--no-ui starts the bridge alone. HUBZOID_DISABLE_EDGE=1 removes the edge and binds Open WebUI to the public port directly, which also removes the public routes to the bridge below.
Edge routes
| Path | Goes to | Present when |
|---|---|---|
/artifacts/... | Bridge | Always. Download links for files the agent saved, signed per Hub. |
/portal/... | Bridge | Always. The Console, its JSON API and the artifact viewer at /portal/artifacts/<id>. |
/mcp | Bridge | MCP_SERVER=true. |
/webhooks/<hub>/... | Inbound server | An inbound surface is enabled. Each delivery is verified before anything runs. |
| Everything else | Open WebUI | Always. |
/v1, /uploads and /healthz stay on the loopback bridge. The edge refuses . and .. path segments and drops client-sent X-Hubzoid-* and X-OpenWebUI-* headers, so a browser cannot assert its own identity.
How each surface reaches the Hub
| Surface | Path into the bridge | Identity and surface name |
|---|---|---|
| Web chat | Open WebUI calls /v1/chat/completions | The signed-in email, surface owui |
| Slack | The adapter calls /v1/chat/completions | slack-dm or slack-channel. The email is resolved when SLACK_IDENTITY_MAPPING=true. |
| WhatsApp, Telegram | The inbound server calls /v1/chat/completions | The sender from the identity/ roster. Unknown senders are rejected first. |
| Generic webhook | The inbound server stores each event as a file for a schedule/ task | No chat reply. A task picks up the events. |
| OpenAI-compatible API | Direct calls to /v1/chat/completions on the host, with a bridge key | The identity headers the calling application sets |
| MCP clients | Edge /mcp, served inside the bridge | The caller's Open WebUI API key, surface mcp |
| Markdown tasks, code workflows | In process, on the workflow engine | The account the run acts as (run_as, else HUBZOID_WORKFLOW_USER, else the owner recorded at setup), surface workflow |
Every surface builds on the same Hub files. Access rules differ per surface: by default only owui, web, api, mcp and workflow may reach restricted tools. See identity and access.
Runtime backends
The runtime is chosen from the resolved model id: MODEL in .env, then the main AGENTS.md model, then claude-local. Loaders, tools, skills and knowledge are the same on all three, so a Hub moves between them by changing MODEL.
| OpenAI Agents SDK | Claude Agent SDK | Local Codex | |
|---|---|---|---|
| Selected when | MODEL is any id that does not start with claude-local or codex-local | MODEL is claude-local or claude-local/<tier> | MODEL is codex-local or codex-local/<model-id> |
| Model calls | LiteLLM, to OpenAI, Anthropic, Azure OpenAI, OpenRouter or another LiteLLM provider | The claude CLI as a subprocess, on a subscription login, CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY | The codex CLI app-server as a subprocess, pinned to version 0.147.0, on a file-backed codex login |
| Hub tools | Function tools on the agent | One in-process MCP server named hubzoid, tools seen as mcp__hubzoid__<name> | Offered to Codex as tools that Hubzoid runs itself |
| Built-in CLI tools | Not applicable | Disabled. Claude Code settings files are not read, and the box's own Claude account connectors are not offered. | Disabled. Each request uses a fresh thread and an isolated configuration with only the login copied in. |
| Delegate sub-agents | Called as tools, each on its own model | Native Claude sub-agents on their own tier | Called as tools, each a separate Codex run on its own model |
On all three, a person's own Open WebUI MCP connections can join their turns, and controlled tools a person may not use are left out. On a Hub whose access is managed in the Console, each connected app also needs its connector_<app> capability. Agents and models covers model ids, delegation and model settings.
Workflow engine
Scheduled work runs on an embedded DBOS (opens in a new tab) engine inside the bridge process. There is no separate scheduler service.
| Work | Starts when | Queue |
|---|---|---|
Markdown tasks (schedule/*.md) and scheduled evals | Their files exist. HUBZOID_DISABLE_SCHEDULE=1 turns them off. | One run at a time per Hub. A due task waits while a chat request is in flight. |
Code workflows (workflows/<name>/*.py) | HUBZOID_SCHEDULES=1 on a single Hub, or automatically under hubzoid gateway. | One run at a time per workflow, side by side across workflows. max_concurrent_workflows in workflows/settings.yaml caps the Hub. |
Markdown tasks are checked every 30 seconds and code workflows every minute. Missed slots are recorded: a markdown task that became due while the Hub was down runs once to catch up, and a code workflow waits for its next slot. DBOS checkpoints each step, so an interrupted code workflow resumes after a restart without repeating finished steps, as long as its code has not changed. Manual runs from hubzoid schedule run go through the same queues. See workflows and schedules.
Databases
A single Hub uses SQLite files inside its folder. Setting DATABASE_URL to a postgresql+psycopg:// URL moves all of them into one PostgreSQL database.
| Store | Default location | Holds |
|---|---|---|
| Operational store | <hub>/.hubzoid/hub.db | Grants, identities, the access change log, tool decisions (hz_access_decisions), usage rows (hz_usage), the workflow catalog, hub.state and hub.shared_state (hz_workflow_kv), artifact records (hz_artifacts) and email deliveries (hz_email_deliveries). Artifact files stay in <hub>/.hubzoid/artifacts/. |
| Hub database | Same file on a single Hub | WhatsApp and Telegram conversation history (hz_inbound_history). |
| Workflow engine | <hub>/.hubzoid/dbos.db | Runs, steps, checkpoints and queues. In PostgreSQL, the dbos schema. |
| Chat app | <hub>/.openwebui-data/webui.db | Accounts, sessions, chats, files and per-user API keys. |
Hubzoid upgrades its own hz_* tables at start and refuses a database written by a newer release. Under hubzoid gateway, the operational store and the chat app database are shared by all Hubs in the gateway's data folder (hubzoid-operational.db and webui.db), while each Hub keeps its own workflow engine file. HUBZOID_OPERATIONAL_DB and HUBZOID_DBOS_DB set those stores separately.
Hosted MCP server
With MCP_SERVER=true, the bridge serves the Hub over MCP Streamable HTTP at /mcp, and the edge exposes it. Callers authenticate with their own Open WebUI API key, never the bridge key. Each call runs under that person's identity on the mcp surface, so restricted tools follow the same grants as chat and every decision is recorded.
The MCP client brings its own model. The Hub serves its tools, including list_knowledge, read_knowledge, list_skills and load_skill, and sends the AGENTS.md body (or the mcp_instructions: frontmatter) as the server's instructions. Tools tied to a chat folder, such as write_artifact and read_upload, and delegate sub-agents are left out. See connect an assistant.
The Console
The bridge serves the Console at /portal/ with a JSON API under /portal/api. It validates the viewer's Open WebUI session on the server, or an Open WebUI API key (Bearer sk-...) on the API, and admits accounts that hold manage_access for the organization or for specific agents. A chat app admin role alone does not open it. Usage and run figures come from Hubzoid's own tables and the workflow engine. People come from the chat app's account directory, read only, and account changes such as Add user and Delete user go through the chat app's own API. Grants made in the Console take effect on the next permission check. Under the gateway, the chat app's model list follows within about 30 seconds. See the Console overview.
Many Hubs behind one gateway
hubzoid gateway ./finance ./operations runs one shared Open WebUI, one edge and one headless bridge per Hub. Each Hub appears in the chat app as its own model, and people see only the agents they may use.
- Edge
- Shared Open WebUI
- Bridge per Hub
- Hub folder
| Path on the gateway | Goes to |
|---|---|
/b/<hub>/artifacts/... | That Hub's bridge |
/b/<hub>/mcp | That Hub's bridge, when its MCP_SERVER=true |
/webhooks/<hub>/... | That Hub's inbound server |
/portal/... | The first Hub's bridge, which serves the organization-wide Console |
Each Hub folder name is its access domain, so names must be unique without regard to case, and each Hub needs its own BRIDGE_PORT. See gateway and topologies.
Next steps
Templates
The three templates bundled with the Hubzoid package and the six role-specific Hubs in the repository, what each contains, and how to start from one.
The Hub
The Hub is the shared foundation behind every Hubzoid agent. Learn what it holds, how context is assembled for each run, and how to scope one or many.
