Hubzoid
Concepts

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.

  1. Surfaces

    Where people and applications send messages.

    • Web chat
    • Slack
    • WhatsApp
    • Telegram
    • Your application
  2. Bridge

    /v1/chat/completions

    One 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, /mcp and scheduled work.

  3. Runtime

    Hub files, tools and skills load the same way on all three.

    • OpenAI Agents SDK
    • Claude Agent SDK
    • Local Codex
  4. Model

    Through LiteLLM, the claude CLI for claude-local, or the codex CLI for codex-local.

    • OpenAI
    • Anthropic
    • Azure OpenAI
    • OpenRouter
    • claude-local
    • codex-local
Assistants connected over MCP enter the bridge at /mcp and bring their own model.

A chat request, end to end

  1. Surface
  2. Edge on :3080
  3. Open WebUI
  4. Bridge /v1
  5. Agent runtime
  6. Model provider
A web chat turn. Other surfaces join at the bridge.
  1. 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.
  2. Open WebUI signs the person in (when sign-in is on), stores the conversation, and calls the bridge's POST /v1/chat/completions with the bridge key and the person's email in forwarded headers.
  3. 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_hub grant before anything runs. It saves any attached files to the chat's upload folder and flattens the message list into one prompt.
  4. 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.
  5. The model provider answers through LiteLLM, through the claude CLI for claude-local, or through the codex CLI for codex-local.
  6. 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.

  1. 1Web chat

    A person asks

    Is bill 1042 a duplicate? Open WebUI sends the conversation to the bridge.

  2. 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.

  3. 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.

  4. 4Model

    Asks for a tool

    Requests ledger_lookup with the bill number. The model only asks. It never runs code.

  5. 5Bridge

    Access check

    The tool comes from restricted/ledger.py, so the guard checks the ledger permission and records the decision before anything runs.

  6. 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_knowledge for the matching rules.

  7. 7Bridge

    The answer streams back

    The model writes the answer. The bridge streams it to the web chat and records usage, never message content.

Other surfaces join at step 2. By default only web chat, API, MCP and workflow callers may run restricted tools.

Processes and ports

hubzoid run my-hub starts these processes. Only the edge port is meant to be reachable from other machines.

ProcessStarted asListens onRole
Bridgeuvicorn hubzoid.server:build_app127.0.0.1:8000 (BRIDGE_PORT, --bridge-port)Agent runtime, access guard, workflow engine, Console and artifact viewer, hosted MCP. Always loopback.
Open WebUIopen-webui serve127.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.
Edgeuvicorn hubzoid.edge:_factoryHUBZOID_HOST:PORT, default 127.0.0.1:3080The single public front door. Routes paths to the bridge or Open WebUI.
Slack adapterhubzoid slack run (with --slack)No port. Slack Socket Mode connects outward.Relays Slack messages to the bridge.
Inbound serverhubzoid 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 browserplaywright-mcp, started by the bridge when HUBZOID_BROWSER=truePort 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

PathGoes toPresent when
/artifacts/...BridgeAlways. Download links for files the agent saved, signed per Hub.
/portal/...BridgeAlways. The Console, its JSON API and the artifact viewer at /portal/artifacts/<id>.
/mcpBridgeMCP_SERVER=true.
/webhooks/<hub>/...Inbound serverAn inbound surface is enabled. Each delivery is verified before anything runs.
Everything elseOpen WebUIAlways.

/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

SurfacePath into the bridgeIdentity and surface name
Web chatOpen WebUI calls /v1/chat/completionsThe signed-in email, surface owui
SlackThe adapter calls /v1/chat/completionsslack-dm or slack-channel. The email is resolved when SLACK_IDENTITY_MAPPING=true.
WhatsApp, TelegramThe inbound server calls /v1/chat/completionsThe sender from the identity/ roster. Unknown senders are rejected first.
Generic webhookThe inbound server stores each event as a file for a schedule/ taskNo chat reply. A task picks up the events.
OpenAI-compatible APIDirect calls to /v1/chat/completions on the host, with a bridge keyThe identity headers the calling application sets
MCP clientsEdge /mcp, served inside the bridgeThe caller's Open WebUI API key, surface mcp
Markdown tasks, code workflowsIn process, on the workflow engineThe 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 SDKClaude Agent SDKLocal Codex
Selected whenMODEL is any id that does not start with claude-local or codex-localMODEL is claude-local or claude-local/<tier>MODEL is codex-local or codex-local/<model-id>
Model callsLiteLLM, to OpenAI, Anthropic, Azure OpenAI, OpenRouter or another LiteLLM providerThe claude CLI as a subprocess, on a subscription login, CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEYThe codex CLI app-server as a subprocess, pinned to version 0.147.0, on a file-backed codex login
Hub toolsFunction tools on the agentOne in-process MCP server named hubzoid, tools seen as mcp__hubzoid__<name>Offered to Codex as tools that Hubzoid runs itself
Built-in CLI toolsNot applicableDisabled. 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-agentsCalled as tools, each on its own modelNative Claude sub-agents on their own tierCalled 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.

WorkStarts whenQueue
Markdown tasks (schedule/*.md) and scheduled evalsTheir 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.

StoreDefault locationHolds
Operational store<hub>/.hubzoid/hub.dbGrants, 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 databaseSame file on a single HubWhatsApp and Telegram conversation history (hz_inbound_history).
Workflow engine<hub>/.hubzoid/dbos.dbRuns, steps, checkpoints and queues. In PostgreSQL, the dbos schema.
Chat app<hub>/.openwebui-data/webui.dbAccounts, 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.

  1. Edge
  2. Shared Open WebUI
  3. Bridge per Hub
  4. Hub folder
One sign-in and one Console in front of several independent Hubs.
Path on the gatewayGoes to
/b/<hub>/artifacts/...That Hub's bridge
/b/<hub>/mcpThat 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

Read this page as Markdown

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.