Hubzoid
Deploy and operate

Multi-hub gateway

Run several hubs behind one chat app, one sign-in and one Console with hubzoid gateway, including its flags, URL layout, data directory and bridges.

hubzoid gateway runs one shared chat app over many hubs. Each hub runs as a headless bridge, appears as its own agent in the chat app's model picker, and keeps its own tools, schedules and access rules. People sign in once, see only the agents they may use, and administrators manage every hub from one Console.

Each Hub keeps its own bridge behind the gateway's single front door.

Who comes in

  • People

    Sign in once and see only the agents they may use.

  • Assistants

    Reach the MCP path of each Hub.

  • Administrators

    Manage every Hub from one Console.

Gateway

//portal//b/<slug>/

One edge on the public port, one shared chat app and one Console.

One bridge per Hub

  • finance

    BRIDGE_PORT=8001

    Its own model, tools and schedules.

  • operations

    BRIDGE_PORT=8002

    Its own .env, knowledge and grants.

  • support

    BRIDGE_PORT=8003

    Its own MCP path at /b/support/mcp when its .env sets MCP_SERVER=true.

Shared data directory

webui.dbhubzoid-operational.db

Accounts, grants, access changes, tool decisions and usage for every Hub. Each Hub keeps its own workflow engine database.

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

When to use this

Use a gateway when several teams share one machine and should share one sign-in: a hub per team (finance, operations, support) with per-team access rather than per-team URLs. It is lighter than one hubzoid run per hub because it starts a single chat app process. When hubs need separate login realms or separate URLs, run them as separate hubs instead.

Quick start

export WEBUI_AUTH=true
export WEBUI_SECRET_KEY='<a random value from openssl rand -hex 32, kept stable>'
export HUBZOID_GATEWAY_ADMIN_EMAIL=operator@example.com
export HUBZOID_GATEWAY_ADMIN_PASSWORD='a-strong-password'

hubzoid gateway ./finance ./operations \
  --data-dir /srv/hubzoid/gateway-data \
  --public-url https://hub.example.com

The shared chat app reads its sign-in settings from the environment the hubzoid gateway process starts with. Each hub's .env configures that hub's bridge (its model, keys and ports) and stays with that hub. Deployments that still keep sign-in and chat app settings (WEBUI_*, OAuth, DEFAULT_USER_ROLE, ENABLE_SIGNUP, HUBZOID_PUBLIC_URL) in a hub's .env keep working: when the gateway's environment lacks one, the gateway takes it from the hub files and names it at start, using the last hub listed if hubs disagree. Move these to the gateway's environment. WEBUI_NAME in a hub's .env never overrides --name. Nothing else in a hub's .env, such as its model keys or restricted-tool secrets, reaches another hub, the chat app or the edge.

Command and flags

hubzoid gateway HUB [HUB ...] [OPTIONS]

Prop

Type

What happens at start

  1. Check hubs
  2. Write manifest
  3. Start bridges
  4. Start chat app
  5. Provision
  6. Start edge
The gateway refuses to start when a check fails, and writes no manifest in that case.

Before anything runs, the gateway 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
  • every hub has a unique BRIDGE_PORT
  • 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 with WhatsApp, Telegram or a generic webhook configured has a unique HUBZOID_INBOUND_PORT
  • a deployment with more than one hub does not point HUBZOID_DBOS_DB at a SQLite file

It then writes the deployment manifest, starts one hubzoid run <hub> --no-ui bridge per hub on its BRIDGE_PORT, waits for each bridge's health check, starts the chat app on a loopback port, provisions each hub in the chat app when credentials are set, and finally starts the edge on the public port. The gateway supervises all of these. When the chat app exits, the gateway stops the rest.

URL layout

Everything is served from one public origin through the edge:

PathServes
/The shared chat app, including its websockets
/portal/The Console for the whole deployment
/b/<slug>/artifacts/...Download links for files made by that hub's agent
/b/<slug>/mcpThat hub's hosted MCP server, only when the hub's own .env sets MCP_SERVER=true
/webhooks/<slug>/<surface>That hub's inbound server, when its .env configures WhatsApp, Telegram or a generic webhook

A hub's slug is its folder name in lower case with every run of other characters replaced by -. When two folders produce the same slug, the second gets -2, the third -3. If that happens to an inbound hub, pin its webhook namespace with HUBZOID_HUB_SLUG in its .env so the inbound server and the edge agree.

When a public URL is set, the gateway gives each bridge it launches HUBZOID_PUBLIC_URL=<public-url>/b/<slug>, so the bridge's download links route back to it through the edge. Leave HUBZOID_PUBLIC_URL out of each hub's .env in a gateway, because a value there wins over the injected one.

The bridges' /v1, /uploads and /healthz routes stay on loopback. The chat app reaches each bridge at http://127.0.0.1:<BRIDGE_PORT>/v1 with that hub's first BRIDGE_API_KEYS entry.

The data directory

--data-dir (default ./.hubzoid-gateway in the working directory) holds the state the hubs share:

FileContents
webui.dbThe chat app's accounts, sessions, chats and model visibility, with its uploads/ and openwebui.log beside it
hubzoid-operational.dbGrants, identities, access changes, tool decisions, usage and workflow state for every hub
deployment.jsonThe deployment manifest: registered hubs, their model ids and database URLs, plus deployment facts such as the configured owner, sign-in flags and whether Open WebUI's user list is hidden, never a secret value (mode 0600)
branding/Optional shared login page and browser tab branding

Each hub keeps its own workflow engine database in <hub>/.hubzoid/dbos.db and a pointer to the manifest in <hub>/.hubzoid/deployment.json. Operator commands such as hubzoid access and hubzoid backup follow that pointer, so they reach the same stores as the running gateway without repeating any database URL. 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 gateway's environment before it starts. Deployment topologies explains why a gateway should use these two variables rather than DATABASE_URL. The manifest can then contain database credentials, so protect it like the .env files.

Bridges the gateway launches

For each bridge it starts, the gateway sets:

VariableValue
HUBZOID_GATEWAY1, which turns on scheduling of code workflows
HUBZOID_OPERATIONAL_DBThe shared operational store
HUBZOID_OWUI_DB<data-dir>/webui.db, where the bridge looks up chat app groups, API keys for MCP and uploaded files
HUBZOID_PUBLIC_URL<public-url>/b/<slug>, when a public URL is known
MCP_SERVER, MCP_ACCESS_GROUPRead from the hub's own .env file only, so one hub's MCP settings never leak into another
OWUI_NATIVE_MCPOne value for the whole gateway, taken from the gateway's environment

The chat app forwards the signed-in person's identity headers to every bridge (ENABLE_FORWARD_USER_INFO_HEADERS defaults to true). Access control depends on them: without them every request reaches the bridge anonymously and every restricted tool is denied.

Bridges as their own services

With --no-bridges, the gateway starts only the chat app and the edge and fronts bridges you run yourself, for example as separate systemd units. Each external bridge must:

  • run on the same machine, as hubzoid run <hub> --no-ui, because the chat app reaches bridges on 127.0.0.1
  • have HUBZOID_GATEWAY=1 and HUBZOID_OWUI_DB=<data-dir>/webui.db in its environment
  • have HUBZOID_PUBLIC_URL=<public-url>/b/<slug> in its environment, since the gateway injects it only into bridges it launches
  • use the same hub paths the gateway was given, and no different operational database in its .env

The gateway writes its manifest and then waits for every bridge's health check, exiting if one never answers. Start the bridges once the manifest exists, so each one finds the shared operational store through its pointer file. A bridge whose HUBZOID_OPERATIONAL_DB disagrees with the manifest refuses to start rather than writing to a second store.

Provisioning each hub

With HUBZOID_GATEWAY_ADMIN_EMAIL and HUBZOID_GATEWAY_ADMIN_PASSWORD set, the gateway signs in to the chat app as that administrator on every start and sets up each hub:

  • The model entry. Picker name and description from AGENTS.md, quick-start suggestions: and an avatar from <hub>/branding/logo.png or another raster logo. These identity fields are refreshed on every start, including removals.
  • A team group. Named after the hub's slug, with read access to that hub's model only. Access is set only when the model entry is first created, so later changes are never overwritten.

Provisioning requires WEBUI_AUTH=true. With sign-in off the gateway skips it and says so, because the chat app would otherwise create its default administrator account. On a fresh data directory the configured account is created as the first administrator. On an existing one the same credentials sign in, and a wrong password is reported instead of creating a stray account. If provisioning cannot run, the gateway logs a warning and starts normally. Leave both variables unset to skip it.

The same service account is used for account lookup and account actions in the Console (Add user, approval, password reset, role changes and Delete user) and for projecting managed grants into the chat app's model visibility, so it must be a chat app administrator. Being a chat app administrator does not make it a Hubzoid administrator. As the configured owner, it becomes the first organization administrator the first time it signs in: see first administrator.

Model visibility and the Console

The gateway keeps the chat app's model access control on, so each team sees only its own agents. It forces BYPASS_MODEL_ACCESS_CONTROL=False even when the variable is set in its environment, and warns when it does. Set HUBZOID_GATEWAY_ALLOW_BYPASS=1 only if you deliberately want every signed-in person to see every agent.

The picker also shows each person only the agents they may enter, chat app administrators included: the edge asks a bridge for that person's entry decision before it returns the model list.

The Console at /portal/ covers every registered hub, including hubs with no grants yet. Every bridge reads the shared operational store, so any bridge can serve it. The edge sends /portal/ and the picker's access check to the first hub's bridge and tries the next one when a bridge refuses connections, so restarting one hub no longer empties the picker. This covers the Console and the picker check only. It is not high availability for the gateway or the chat app.

Administrators and people with Manage access see Admin Console above their profile in the chat sidebar. Once a hub's access moves to managed grants, the edge blocks model access edits for that hub in the chat app and points to the Console, while chat app groups keep working for hubs that have not moved.

A gateway set up fresh with Console accounts (no earlier deployment.json or webui.db, WEBUI_AUTH=true and the service account set) also hides Open WebUI's user list and refuses its account admin writes from the browser, so accounts are managed under People in the Console. Existing deployments keep the list. HUBZOID_HIDE_OWUI_USERS=true or false in the gateway's environment overrides either. See hide the Open WebUI user list.

Branding

The login page, favicon and tab title are shared by the whole gateway. The gateway takes them from the first match of:

  1. HUBZOID_GATEWAY_BRANDING, set to a hub slug or to a directory that contains a branding/ folder
  2. <data-dir>/branding/, when it contains files
  3. the first hub's branding/ folder

Each hub's own logo appears as its agent avatar through provisioning. Unlike a single hub, the gateway keeps the chat app's Workspace menu visible, because administrators manage groups there.

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.