Several Hubs
Serve several Hubs behind one address and one sign-in with a hubzoid.yaml, then control each Hub while the others keep answering.
At the end of this guide several Hubs run behind one address. People sign in once and see only the agents they may use, administrators manage every Hub from one Settings page, and each Hub restarts on its own while the others keep answering.
hubzoid run does this from a hubzoid.yaml that lists the Hubs. Each Hub runs as its own process and keeps its own model, tools, schedules and access rules. All of them share one operational store for accounts, sessions, conversations and grants.
Who comes in
People
Sign in once and see only the agents they may use.
Assistants
Connect to the MCP path of each Hub, with OAuth.
Administrators
Manage every Hub from Settings.
Edge
//settings/b/<slug>/
One edge on the public port. The web app and Settings come from the bridges.
One process per Hub
finance
127.0.0.1:8000Its own model, tools and schedules.
operations
127.0.0.1:8001Its own
.env, knowledge and grants.support
127.0.0.1:8002Its own MCP path at
/b/support/mcpwhen its.envsetsMCP_SERVER=true.
Shared data folder
deployment.jsonhubzoid-operational.dbsecret.key
Accounts, sessions, conversations, grants, usage and workflow state for every Hub, and the deployment key. Each Hub keeps its own workflow engine database.
Run Hubs together when several teams share one machine and should share one sign-in, with access set per team rather than per URL. Hubs that run together share one login surface by design. When Hubs need separate login realms or separate URLs, run them as separate Hubs instead.
What you need
- Hubzoid installed on the server, as in single server.
- The Hub folders side by side in one parent folder, each with its
AGENTS.mdand its own.envfor its model and keys. - Folder names that differ even ignoring case, and a different
name:in eachAGENTS.md.
Run several Hubs together
List the Hubs in hubzoid.yaml
Create hubzoid.yaml in the folder that holds the Hub folders:
version: 1
public_url: https://agents.example.com
sign_in: true
owner: sam@example.com
hubs: # the first also answers at /
- finance
- operationsPut deployment settings in the .env next to it
Secrets and switches for the whole deployment go in the .env beside hubzoid.yaml: HUBZOID_SECRET_KEY, database URLs, sign-in provider settings, ENABLE_SIGNUP and mail. This file wins over the process environment. For the first start only, add the owner's password:
HUBZOID_ADMIN_PASSWORD=<at least 8 characters>Each Hub's own .env configures that Hub only and stays with it. Sign-in is set once: a Hub .env that sets HUBZOID_AUTH or HUBZOID_SECRET_KEY differently stops the start with the reason. Leave HUBZOID_PUBLIC_URL out of the Hub files, because public_url sets it for every Hub.
Start the deployment
In the folder with hubzoid.yaml:
hubzoid runhubzoid run checks every Hub, starts each in its own process on a free loopback port, then starts the edge:
✓ Hubzoid is ready: http://127.0.0.1:3080 (sign-in on)hubzoid status in the same folder lists each Hub with its state:
finance ready since 09:14:02 restarts 0
operations ready since 09:14:05 restarts 0Sign in and grant access
Open the public address and sign in as the owner. Set up two-step sign-in when asked, then remove HUBZOID_ADMIN_PASSWORD from .env.
The first start makes the owner an administrator with the owner's access on every Hub. Add people in Settings → People and grant Use this agent on each Hub they need. Being an administrator does not by itself grant every agent or every tool. See access administration.
Run it as a service
One systemd unit runs the whole deployment. In the folder with hubzoid.yaml:
sudo /opt/hubzoid/agents/.venv/bin/hubzoid service install --port 3080 --user hubzoid --write
sudo systemctl daemon-reload
sudo systemctl enable --now hubzoidAdd the hardening lines and the reverse proxy from single server.
Control a running deployment
Each Hub is its own process, so you can restart, reload or stop one while the other agents keep answering.
How a Hub that exits is restarted
A Hub that exits is started again after 1 second, then after twice as long each time, up to 60 seconds. After 5 failed starts in 10 minutes it is marked failed and stays stopped until hubzoid start <hub>. Meanwhile its pages answer 503 "This agent is restarting", and the other agents keep answering.
Run these in the deployment folder, or name its data folder with --data-dir:
| Command | What it does |
|---|---|
hubzoid status | Each Hub's state, restarts and last exit, and whether it needs a reload |
hubzoid restart <hub> | Drains the Hub, then restarts it. The others keep running. |
hubzoid reload <hub> | Checks the Hub's folder, then restarts it. A folder with problems changes nothing. |
hubzoid stop <hub>, hubzoid start <hub> | Stops a Hub until it is started again, or starts a stopped or failed one |
hubzoid logs <hub> -f | That Hub's output, including its channels. edge names the front door. |
When changes take effect, and Hub commands in the deployment folder
A restart drains first: the Hub takes no new replies, and running ones get --grace seconds (default 120) to finish. Changes to brand:, chat: and diagnostics: in hubzoid.yaml are read again by every agent when the file changes. A Hub's own files (instructions, skills, channels, connectors, .env, tools_local/) take effect with hubzoid reload <hub>, and hubzoid status says when one is needed.
Hub commands such as hubzoid admin list, hubzoid audit and hubzoid backup work in the deployment folder too. They use the deployment's public_url and sign-in settings, and ask which Hub to use when there are several: hubzoid audit finance.
The deployment file
hubzoid.yaml needs only version and hubs. The other keys set the public address, sign-in, the owner and shared defaults.
Every key in hubzoid.yaml
Prop
Type
hubzoid run options for a deployment
Prop
Type
hubzoid run ./finance ./operations runs the same Hubs without a file. Bridge ports are assigned for you from 8000 up, skipping ports another program uses. A BRIDGE_PORT in a Hub's .env is still honoured.
What happens at start
- Check Hubs
- Write manifest
- Prepare deployment key
- Start Hubs
- Start edge
Before anything runs, hubzoid run checks the Hub folders, names, ports and sign-in settings.
Every check before start
hubzoid run checks that:
- every path has an
AGENTS.md - Hub folder names are unique ignoring case, because a folder name is the Hub's access domain
- no two Hubs pin the same
BRIDGE_PORTorHUBZOID_INBOUND_PORTin their.env - every Hub surfaces as a unique model id, from
name:in itsAGENTS.mdorMODEL_LABELin its.env, so one team's chats can never route to another team's agent - every Hub that sets
MCP_SERVER=truehas anMCP_PUBLIC_URLending in/b/<slug>/mcp - no Hub's
.envsets a differentHUBZOID_AUTHorHUBZOID_SECRET_KEYfrom the deployment's HUBZOID_DBOS_DBdoes not point several Hubs at one SQLite file- with sign-in off, the edge stays on loopback unless
HUBZOID_ALLOW_UNAUTHENTICATED_NETWORK=true
It then writes the deployment manifest, creates the deployment key if it does not exist, starts each Hub on a loopback port, waits for each Hub's health check, and starts the edge. A Hub that fails to start does not stop the others.
URL layout
Everything is served from one public origin through the edge:
| Path | Serves |
|---|---|
/ | The web app: chat, sign-in at /auth and each person's account and connections |
/settings | Settings for the whole deployment |
/b/<slug>/api/... | That Hub's part of the web app, such as its chat calls |
/b/<slug>/artifacts/... | Download links for files made by that Hub's agent |
/b/<slug>/mcp | That Hub's hosted MCP server, only when its own .env sets MCP_SERVER=true |
/b/<slug>/v1 | That Hub's OpenAI-compatible API, with a personal API key |
/webhooks/<slug>/<name> | That Hub's named workflow webhooks |
/webhooks/<slug>/<surface> | That Hub's inbound server, for WhatsApp, Telegram or a generic webhook |
/healthz/workflows | Answered by the edge: 503 while any Hub's workflow engine is unhealthy |
Fallback routing and Hub slugs
Paths that name no Hub go to the first Hub's bridge. When the first bridge refuses connections the edge tries the next one, so the web app and Settings stay reachable while one Hub restarts. This is not high availability for the deployment.
A Hub's slug is its folder name in lower case, with every run of other characters replaced by -. When different names produce the same slug, such as stock_desk and stock-desk, the second gets -2. HUBZOID_HUB_SLUG in a Hub's .env fixes the path segment. When a public URL is set, hubzoid run gives each Hub HUBZOID_PUBLIC_URL=<public-url>/b/<slug>, so its links route back to it.
At more than one address
When people reach the deployment at more than one address, list the others in HUBZOID_ALLOWED_ORIGINS in the .env next to hubzoid.yaml, comma-separated:
HUBZOID_PUBLIC_URL=https://agents.example.com
HUBZOID_ALLOWED_ORIGINS=https://agents.internal.example.comWhich addresses are accepted, and identity provider redirects
Every agent then accepts each address the deployment names: public_url in hubzoid.yaml and HUBZOID_ALLOWED_ORIGINS, and each Hub's own HUBZOID_PUBLIC_URL and HUBZOID_ALLOWED_ORIGINS. Sign-in and connector sign-in return to the address they started on, so register a redirect URI for each address at your identity providers. hubzoid run records the addresses when it starts, so restart it after adding one.
The data directory
--data-dir (default .hubzoid/ next to hubzoid.yaml) holds the state the Hubs share. Hubs named on the command line without a file use .hubzoid-gateway/ in the working folder, and a deployment that already has .hubzoid-gateway/ next to hubzoid.yaml keeps using it.
Every file in the data directory, and what each Hub keeps
| File | Contents |
|---|---|
hubzoid-operational.db | Accounts, sessions, conversations, connector credentials, grants, access changes, tool decisions, usage and workflow state for every Hub |
deployment.json | The manifest: registered Hubs, their model ids and database URLs, whether sign-in is on, the owner, the allowed addresses and sign-in flags. It never holds a secret value, but it can hold a database URL with a password, so it is written with mode 0600. |
secret.key | The deployment key, unless HUBZOID_SECRET_KEY provides it. It encrypts stored connector credentials and signs identity between Hubzoid processes. Back it up apart from the data. |
logs/ | Each Hub's and the edge's output, which hubzoid logs reads |
Each Hub keeps its own workflow engine database in <hub>/.hubzoid/dbos.db, its chat files under <hub>/.hubzoid/chats/, and a pointer to the manifest in <hub>/.hubzoid/deployment.json. Operator commands such as hubzoid admin, hubzoid grant and hubzoid backup follow that pointer, so they reach the same stores as the running deployment. Keep Hub folders out of the data directory, because hubzoid backup refuses a data directory that contains a Hub.
To use PostgreSQL, set HUBZOID_OPERATIONAL_DB and HUBZOID_DBOS_DB in the .env next to hubzoid.yaml before the first start. Deployment topologies explains why several Hubs should use these two rather than DATABASE_URL.
What each Hub gets
For each Hub it starts, hubzoid run sets the shared operational store, the public URL and the deployment's sign-in, and keeps the bridge on loopback.
Every variable set for each Hub, and what Hubs inherit
| Variable | Value |
|---|---|
HUBZOID_GATEWAY | 1, which also turns on scheduling of code workflows |
HUBZOID_OPERATIONAL_DB | The shared operational store |
HUBZOID_PUBLIC_URL | <public-url>/b/<slug>, when a public URL is known |
HUBZOID_AUTH | The deployment's sign-in, the same for every Hub |
HUBZOID_HOST | 127.0.0.1. Bridges never listen on a public interface. |
MCP_SERVER, MCP_PUBLIC_URL | Read from the Hub's own .env file only, so one Hub's MCP settings never leak into another |
Hubs also inherit the rest of the deployment's environment, including the .env next to hubzoid.yaml, so sign-in provider settings set there reach the bridges that serve the sign-in pages. From a deployment secret in AWS Secrets Manager, bridges take only a fixed list of deployment keys. Each bridge then loads its own Hub .env and its own Hub and restricted secrets.
Older deployments may still keep sign-in settings (OAuth settings, ENABLE_SIGNUP, HUBZOID_PUBLIC_URL) in a Hub's .env. When the deployment's own settings lack one, hubzoid run takes it from the Hub files and names it at start. Move these to the .env next to hubzoid.yaml.
Branding
The brand is the brand: section of hubzoid.yaml, with its images in a branding/ folder next to it. It covers the whole deployment, and Settings shows it read-only under Organization → Brand. brand.identity.name is the deployment's display name. Each agent keeps its own name and avatar in the agent switcher.
Moving from hubzoid gateway
hubzoid gateway <hubs> still works for now. It prints the hubzoid.yaml that says the same, then runs it, and it will be removed. Bridges run as separate units (--no-bridges) are gone. To move a running gateway to one hubzoid run, follow upgrading a gateway.
If something goes wrong
| What you see | What to do |
|---|---|
| The start stops and names a Hub setting that differs | A Hub's .env sets HUBZOID_AUTH or HUBZOID_SECRET_KEY differently. Remove it from the Hub file and set it once next to hubzoid.yaml. |
| The start stops on a duplicate name or model id | Rename one Hub folder, or give it a different name: in AGENTS.md. |
| One agent answers "This agent is restarting" | hubzoid status shows it restarting or failed, with the reason. Fix it, then hubzoid start <hub>. |
hubzoid status says reload needed | Run hubzoid reload <hub>. |
| Signed in, but an agent is missing | The account has no Use this agent grant on that Hub. Grant it in Settings. |
hubzoid backup refuses the data directory | A Hub folder sits inside it. Move the Hub out. |

