Architecture
How a request moves through Hubzoid, from the web app and other surfaces through the edge and the bridge to the agent runtime and the model.
Hubzoid runs each Hub as one bridge process behind a single public front door, the edge. Every surface, from the web app to Slack and connected assistants, reaches the Hub through that bridge, which knows who is asking, checks their access, runs the agent and streams the answer back.
When to read this
You do not need this page to build an agent. Read it when you plan a deployment, open a port, choose a database, or want to know where a decision such as "may Priya run this tool?" is enforced.
What starts when you run a Hub
hubzoid run my-hub✓ Hubzoid is ready: http://127.0.0.1:3080 (local mode, sign-in off)
Connect Claude Code: claude mcp add --transport http my-hub http://127.0.0.1:3080/mcpThat one command started:
- the edge on port 3080, the only port meant for browsers and other machines
- the bridge for
my-hubon127.0.0.1, from port 8000 up, skipping ports already in use - the workflow engine inside the bridge, for scheduled work
- SQLite files under
my-hub/.hubzoid/for conversations, access and runs
How it works
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 app
- Your own app
- Slack
- Telegram
- OpenAI-compatible API
Seam one: every surface hands the bridge one verified person and one conversation
Bridge
/api/conversations/api/agui/v1/chat/completionsOne process per Hub, on loopback behind the edge. It identifies the caller, runs the Hub agent, guards restricted tools and streams the answer. It also serves the web app, Settings,
/mcpand workflows.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.
Video lesson 4 · 3:22Three types of AI agents, one brainChoose what starts a task: a person, a trigger or an assistant.
A chat request, end to end
- Browser
- Edge on :3080
- Bridge
- Agent runtime
- Model provider
What each process does during the turn
- The browser reaches the edge on the public port (
PORT, default 3080). The edge passes every page and API call to the bridge, which serves the web app itself. - The bridge knows who is asking from the Hubzoid session, or, with sign-in off, treats the request as the local owner
admin@localhost. It requires Use this agent before anything runs, stores the message, saves attached files to the conversation and builds the prompt from the active branch of the conversation, with the person's own notes first. - The runtime runs the Hub's agent in a server task that outlives the page. Closing the browser does not stop a reply, and Stop does. Restricted tools pass through the access guard, which records every allow or deny decision before a call runs.
- The model provider answers through LiteLLM, the
claudeCLI or thecodexCLI. - The bridge streams the reply with its tool steps, stores it in the conversation 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 app turn on a Hub with a restricted ledger_lookup tool.
- 1Web app
A person asks
Is bill 1042 a duplicate? The web app sends the message to the bridge.
- 2Bridge
Who is asking
Reads the signed-in person from the session, checks Use this agent, and builds the prompt from the conversation.
- 3Runtime
The agent takes the turn
The Hub agent 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 app, stores it and records usage, never message content.
Processes and ports
Only the edge port is meant to be reachable from other machines. The bridge always listens on loopback behind it.
Every process, its port and when it starts
| Process | Listens on | Role |
|---|---|---|
| Edge | HUBZOID_HOST:PORT, default 127.0.0.1:3080 | The single public front door. Routes paths to a bridge or the inbound server. |
| Bridge | 127.0.0.1, BRIDGE_PORT or the first free port from 8000 | Web app and sign-in, agent runtime, access guard, workflow engine, Settings and hosted MCP. Always loopback. |
| Slack adapter | No port. Slack Socket Mode connects outward. | Relays Slack messages to the bridge. Starts when a file in the Hub's channels/ sets provider: slack_assistant. |
| Inbound server | 127.0.0.1:8100 (HUBZOID_INBOUND_PORT) | Verifies and handles WhatsApp, Telegram and generic webhook deliveries. Starts when a channel file turns one on. |
| Shared browser | Port 8931 (HUBZOID_BROWSER_PORT) | One resource-limited browser shared by every agent as MCP tools, when HUBZOID_BROWSER=true. |
With sign-in off, the edge stays on 127.0.0.1, and hubzoid run refuses another interface unless HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true accepts that anyone who can reach the port acts as the Hub's owner.
What the edge routes
The edge sends the web app, sign-in, Settings, downloads and /mcp to the bridge, and webhook deliveries under /webhooks/<hub>/ to the bridge or the inbound server. HTTP endpoints lists every route.
Health checks, internal paths and identity headers
It answers /healthz/workflows itself, with 503 while a Hub's workflow engine is unhealthy, for an external uptime monitor. The bridge's internal API (/v1, /uploads, /otel) stays on loopback, and the edge answers 404 for it.
The edge refuses . and .. path segments and drops client-sent X-Hubzoid-* identity headers, so a browser cannot assert its own identity.
How each surface reaches the Hub
Every surface builds on the same Hub files. Access rules differ per surface. See identity and access, and chat inside your own app for the AG-UI surface.
Each surface, its path into the bridge and who the caller is
| Surface | Path into the bridge | Who the caller is |
|---|---|---|
| Web app | The bridge's conversation API, in process | The signed-in account, or the local owner with sign-in off. Surface web. |
| Your own app | AG-UI at /api/agui, with a pass your app's server signs | The person your app vouches for, limited to the agents and tools the app allows. Surface app. |
| Slack | The adapter calls /v1/chat/completions with a signed identity assertion | With SLACK_IDENTITY_MAPPING=true, the sender's email, which must belong to a Hubzoid account. Surfaces slack-dm and slack-channel. |
| WhatsApp, Telegram | The inbound server calls /v1/chat/completions with a signed identity assertion | The person whose phone number is recorded in Settings, else the Hub's identity/access.py lookup. 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. |
| Named webhook | The bridge stores each event and starts the code workflow that consumes it | The workflow's account. Surface workflow. |
| OpenAI-compatible API | Direct calls to /v1/chat/completions on loopback | Identity headers count only with a valid identity assertion signed with the deployment key. A call without one names no person and is refused. |
| MCP clients | Edge /mcp, served inside the bridge | The account that signed in and approved the connection. Surface mcp. |
| Markdown tasks, code workflows | In process, on the workflow engine | The account the run acts as. Surface workflow. |
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.
How the three runtimes differ
| 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 | The codex CLI app-server as a subprocess, Codex CLI 0.163.0 or 0.147.0 |
| 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 |
| The CLI's own tools | Not applicable | Disabled. Claude Code settings files and the machine's own Claude connectors are not used. | Disabled. Each request uses a fresh thread and an isolated configuration with only the login copied in. |
Agents and models covers model ids, sign-in for each runtime, delegation and model settings.
Workflow engine
Workflows run on an embedded DBOS (opens in a new tab) engine inside the bridge process. There is no separate scheduler service, and one engine owns each Hub at a time. See workflows and schedules.
When tasks and code workflows run, and how they resume
- Markdown tasks (
schedule/*.md) and scheduled evals run whenever their files exist, one at a time per Hub, and wait while a chat request is in flight. - Code workflows (
workflows/<name>/*.py) start on their own only withHUBZOID_SCHEDULES=1on a single Hub, or automatically in a deployment thathubzoid runstarts fromhubzoid.yamlor with several Hubs.
DBOS checkpoints each step, so an interrupted code workflow resumes after a restart without repeating finished steps.
Databases
A single Hub uses SQLite files inside its folder. Setting DATABASE_URL to a postgresql+psycopg:// URL moves them into one PostgreSQL database.
Every store, what it holds and how several Hubs share them
| Store | Default location | Holds |
|---|---|---|
| Operational store | <hub>/.hubzoid/hub.db | Accounts and sessions, web app conversations (hz_conversations, hz_messages), grants, phone numbers, the access change log, tool decisions (hz_access_decisions), usage rows (hz_usage), each person's notes, connectors and personal connection tokens, MCP OAuth grants, workflow state (hz_workflow_kv), personal schedules, report series, artifact records and 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. |
| Deployment key | <hub>/.hubzoid/secret.key, or HUBZOID_SECRET_KEY | Encrypts stored credentials and signs identity assertions between Hubzoid's processes. Back it up apart from the data. |
Hubzoid upgrades its own hz_* tables at start and refuses a database written by a newer release. When several Hubs run together, they share one operational store in the deployment's state folder (hubzoid-operational.db), with the deployment key beside it, while each Hub keeps its own workflow engine file. HUBZOID_OPERATIONAL_DB and HUBZOID_DBOS_DB set those stores separately.
Hosted MCP server
The bridge serves the Hub over MCP Streamable HTTP at /mcp, and the edge exposes it. hubzoid run turns it on for a Hub served on this machine or behind an https HUBZOID_PUBLIC_URL, and MCP_SERVER in the Hub's .env overrides that. See connect an assistant.
Sign-in, access rules and which tools MCP clients get
- Sign-in is OAuth only. The MCP client opens Hubzoid's sign-in and consent page, and Hubzoid issues tokens for that person. Access tokens last 10 minutes, and refresh tokens and grants up to 30 days. Bridge keys are never accepted.
- The same rules as chat. Each call runs as that person on the
mcpsurface, so restricted tools follow the same grants and every decision is recorded. - The client brings its own model. The Hub serves its tools and sends the
AGENTS.mdbody, or themcp_instructions:frontmatter, as the server's instructions. - Some tools stay in chat. Tools tied to a chat (
write_artifact,list_artifacts,read_upload), the memory and hub change tools such asremember,call_jevand delegate sub-agents are left out.
People revoke a connection at /mcp/oauth/connections.
Settings
The bridge serves Settings at /settings, part of the web app, with a JSON API under /portal/api. It opens from the account menu, and each person sees what their access allows:
- Personal, for everyone: Account, Connected accounts, Sources, Memory and My schedules.
- Manage, for organization administrators and people who manage an agent: Agents, Activity, Team schedules, People, Usage and Organization. With sign-in off there are no People or Team schedules pages.
Grants made in Settings take effect on the next permission check, in every bridge that shares the database. See the Settings overview.
Several Hubs in one run
hubzoid run ./finance ./operations runs one edge and one bridge per Hub, each in its own process. Plain hubzoid run in a folder with a hubzoid.yaml starts the Hubs it lists. The bridges share the operational store, so one sign-in covers every agent a person may use, and the web app's agent switcher lists only those agents.
- Edge
- Bridge per Hub
- Hub folder
See running several Hubs.
Paths per Hub, folder names and restarts
Each Hub's own paths are served under /b/<hub>/, such as /b/<hub>/mcp for its MCP server. Everything else, including the web app and /settings, goes to the first Hub's bridge, with the others as fallbacks.
Each Hub folder name is its access domain, so names must be unique without regard to case. A Hub that stops is started again on its own. While it restarts its pages answer 503, and the other agents keep answering.

