Deploy and operate

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:8000

    Its own model, tools and schedules.

  • operations

    127.0.0.1:8001

    Its own .env, knowledge and grants.

  • support

    127.0.0.1:8002

    Its own MCP path at /b/support/mcp when its .env sets MCP_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.

Bridges stay on loopback. Only the edge listens on the public port.

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.md and its own .env for its model and keys.
  • Folder names that differ even ignoring case, and a different name: in each AGENTS.md.

Run several Hubs together

List the Hubs in hubzoid.yaml

Create hubzoid.yaml in the folder that holds the Hub folders:

hubzoid.yaml
version: 1
public_url: https://agents.example.com
sign_in: true
owner: sam@example.com
hubs: # the first also answers at /
  - finance
  - operations

Put 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:

.env next to hubzoid.yaml
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:

Terminal
hubzoid run

hubzoid run checks every Hub, starts each in its own process on a free loopback port, then starts the edge:

Output
✓ Hubzoid is ready: http://127.0.0.1:3080  (sign-in on)

hubzoid status in the same folder lists each Hub with its state:

Output
finance                  ready       since 09:14:02  restarts 0
operations               ready       since 09:14:05  restarts 0

Sign 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:

Terminal
sudo /opt/hubzoid/agents/.venv/bin/hubzoid service install --port 3080 --user hubzoid --write
sudo systemctl daemon-reload
sudo systemctl enable --now hubzoid

Add 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:

CommandWhat it does
hubzoid statusEach 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> -fThat 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

  1. Check Hubs
  2. Write manifest
  3. Prepare deployment key
  4. Start Hubs
  5. Start edge
hubzoid run refuses to start when a check fails, and writes no manifest in that case.

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_PORT or HUBZOID_INBOUND_PORT in their .env
  • every Hub surfaces as a unique model id, from name: in its AGENTS.md or MODEL_LABEL in its .env, so one team's chats can never route to another team's agent
  • every Hub that sets MCP_SERVER=true has an MCP_PUBLIC_URL ending in /b/<slug>/mcp
  • no Hub's .env sets a different HUBZOID_AUTH or HUBZOID_SECRET_KEY from the deployment's
  • HUBZOID_DBOS_DB does 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:

PathServes
/The web app: chat, sign-in at /auth and each person's account and connections
/settingsSettings 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>/mcpThat Hub's hosted MCP server, only when its own .env sets MCP_SERVER=true
/b/<slug>/v1That 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/workflowsAnswered 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:

.env next to hubzoid.yaml
HUBZOID_PUBLIC_URL=https://agents.example.com
HUBZOID_ALLOWED_ORIGINS=https://agents.internal.example.com
Which 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
FileContents
hubzoid-operational.dbAccounts, sessions, conversations, connector credentials, grants, access changes, tool decisions, usage and workflow state for every Hub
deployment.jsonThe 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.keyThe 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
VariableValue
HUBZOID_GATEWAY1, which also turns on scheduling of code workflows
HUBZOID_OPERATIONAL_DBThe shared operational store
HUBZOID_PUBLIC_URL<public-url>/b/<slug>, when a public URL is known
HUBZOID_AUTHThe deployment's sign-in, the same for every Hub
HUBZOID_HOST127.0.0.1. Bridges never listen on a public interface.
MCP_SERVER, MCP_PUBLIC_URLRead 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 seeWhat to do
The start stops and names a Hub setting that differsA 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 idRename 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 neededRun hubzoid reload <hub>.
Signed in, but an agent is missingThe account has no Use this agent grant on that Hub. Grant it in Settings.
hubzoid backup refuses the data directoryA Hub folder sits inside it. Move the Hub out.

Next steps

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.