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=8001Its own model, tools and schedules.
operations
BRIDGE_PORT=8002Its own
.env, knowledge and grants.support
BRIDGE_PORT=8003Its own MCP path at
/b/support/mcpwhen its.envsetsMCP_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.
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.comThe 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
- Check hubs
- Write manifest
- Start bridges
- Start chat app
- Provision
- Start edge
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 itsAGENTS.mdorMODEL_LABELin 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_DBat 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:
| Path | Serves |
|---|---|
/ | 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>/mcp | That 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:
| File | Contents |
|---|---|
webui.db | The chat app's accounts, sessions, chats and model visibility, with its uploads/ and openwebui.log beside it |
hubzoid-operational.db | Grants, identities, access changes, tool decisions, usage and workflow state for every hub |
deployment.json | The 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:
| Variable | Value |
|---|---|
HUBZOID_GATEWAY | 1, which turns on scheduling of code workflows |
HUBZOID_OPERATIONAL_DB | The 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_GROUP | Read from the hub's own .env file only, so one hub's MCP settings never leak into another |
OWUI_NATIVE_MCP | One 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 on127.0.0.1 - have
HUBZOID_GATEWAY=1andHUBZOID_OWUI_DB=<data-dir>/webui.dbin 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-startsuggestions:and an avatar from<hub>/branding/logo.pngor 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:
HUBZOID_GATEWAY_BRANDING, set to a hub slug or to a directory that contains abranding/folder<data-dir>/branding/, when it contains files- 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
Docker
Build or pull the Hubzoid image, run a hub with Docker Compose on SQLite or PostgreSQL, and keep its state on a mounted hub folder.
Authentication
Configure sign-in for the chat app and the Console with email and password, Google, Microsoft, GitHub, OIDC, LDAP or a trusted proxy header, and manage accounts in the Console.
