Deployment topologies
The shapes a Hubzoid deployment can take, which processes each one runs, where it keeps its data, and when to choose PostgreSQL.
A deployment topology is how many Hubs share one Hubzoid deployment, and where their data lives. Every shape is built from the same three processes, so choosing one is mostly a choice about sign-in and storage.
When to use it
Read this before your first deployment past a laptop, and again when you add a second Hub, move to PostgreSQL or run Hubzoid on a container platform. Pick the shape before you provision a server, because moving an existing deployment from SQLite to PostgreSQL is a manual step.
| Situation | Choose |
|---|---|
| One team, one agent, one server | One Hub on SQLite, behind a reverse proxy |
| Several teams on one server who should sign in once | Several Hubs in one hubzoid run, on SQLite |
| Hubs that must keep separate accounts and URLs | Separate hubzoid run processes, one per Hub |
| A managed database, point-in-time recovery, or more than one machine | Either shape with PostgreSQL |
pip install fails on the host, or your platform runs containers | The Docker image, one container per Hub |
The smallest real example
One Hub on one machine is a single command. It starts every process the Hub needs and keeps the Hub's data in its own folder:
hubzoid run my-hub✓ Hubzoid is ready: http://127.0.0.1:3080 (local mode, sign-in off)Everything this Hub writes lands in my-hub/.hubzoid/. Turn sign-in on and put a TLS proxy in front, and the same command serves a team. Single server walks through that.
How it works
- Browser
- TLS proxy
- Edge
- Bridge
- Model provider
hubzoid run starts and supervises these processes. A process that exits is started again on its own.
| Process | Listens on | What it does |
|---|---|---|
| Edge | The public port, PORT (default 3080), on --host (default 127.0.0.1) | The single front door. Sends requests to the Hub's bridge and inbound messages to the inbound server. Refuses the bridge's internal API. |
| Bridge | 127.0.0.1, from port 8000 up | One per Hub. Runs the agent, its tools, workflows and schedules, the web app with Settings, and the hosted MCP server. Busy ports are skipped. |
| Inbound server | 127.0.0.1:HUBZOID_INBOUND_PORT (default 8100) | Only when a WhatsApp, Telegram or generic webhook channel is configured. |
The bridge never listens on a public interface. The edge answers 404 for the bridge's internal API (/v1, /uploads and /otel), which only Hubzoid's own processes use on the same machine. The one exception is the OpenAI-compatible chat API (/v1/chat/completions, /v1/models), which the edge forwards only with a personal API key. See the security model for every public path.
Supported shapes
| Shape | How you run it | Accounts and sign-in |
|---|---|---|
| One Hub | hubzoid run <hub> | Its own accounts, in the Hub's operational store |
| Separate Hubs on one machine | One hubzoid run per Hub, each with its own PORT | Separate accounts, sign-in and URL for each Hub |
| Several Hubs together | hubzoid run with a hubzoid.yaml, or hubzoid run <hub> <hub> | One set of accounts, one sign-in and one Settings page for every Hub |
| Container | The Hubzoid image, which runs hubzoid run /hub | As for one Hub |
| A hosting provider | hubzoid deploy init railway (or maritime) writes the files | As for one Hub. Not yet verified on either provider's live service. |
- TLS proxy
- Edge
- Bridge for each Hub
- Shared operational store
Hubs that run together share one login surface by design. When Hubs need separate login realms, run them as separate Hubs instead. Several Hubs covers hubzoid.yaml and the URL layout.
Rules for every shape
- One bridge per Hub. Two processes serving the same Hub at once is not supported. On a container platform, run each Hub as its own service with a desired count of 1 and no autoscaling.
- SQLite stays on a local disk. Do not put SQLite files on a network filesystem such as NFS or EFS, because file locking over the network corrupts them. Use PostgreSQL when processes run on different machines.
- One SQLite workflow database per Hub. When several Hubs run together, each keeps its own
.hubzoid/dbos.db.hubzoid runrefuses to start a deployment of several Hubs whoseHUBZOID_DBOS_DBnames a SQLite file. - Unique Hub folder names. A Hub's folder name is its access domain. Within one deployment, names must differ even ignoring case.
- SQLite 3.42 or newer on Python 3.12. The workflow engine needs it for schedules and workflows.
hubzoid doctorreports it asdeps.sqlite. Python from python.org, uv or Homebrew, Debian 13 and Ubuntu 24.04 ship a new enough SQLite. PostgreSQL avoids the requirement.
There is no certified maximum number of Hubs or people. Size the machine for the workload you run, and rehearse a backup, a restore and an interrupted workflow on a copy of the deployment before rollout.
Where data lives
| Store | What it holds | One Hub (SQLite) | Several Hubs (SQLite) |
|---|---|---|---|
| Operational store | Accounts, sessions, conversations, connector credentials, grants, access changes, tool decisions, usage, workflow state, webhook events and alerts | <hub>/.hubzoid/hub.db | <data-dir>/hubzoid-operational.db, shared by every Hub |
| Workflow engine | Run history and checkpoints for schedules and workflows | <hub>/.hubzoid/dbos.db | <hub>/.hubzoid/dbos.db for each Hub |
| Hub database | WhatsApp and Telegram conversation history | <hub>/.hubzoid/hub.db | <hub>/.hubzoid/hub.db for each Hub |
| Files | Chat uploads and files the agent made | <hub>/.hubzoid/chats/, <hub>/output/ | The same, in each Hub |
| Deployment key | Encrypts stored credentials and signs messages between Hubzoid processes | <hub>/.hubzoid/secret.key, or HUBZOID_SECRET_KEY | <data-dir>/secret.key, or HUBZOID_SECRET_KEY |
How Hubzoid finds each store
In a standalone Hub the operational store and the Hub database share one SQLite file. Each keeps its own schema version table, so they never collide.
Hubzoid finds each store in a fixed order:
| Store | Resolution order |
|---|---|
| Operational store | The registered deployment manifest, then HUBZOID_OPERATIONAL_DB, then DATABASE_URL, then the Hub's own SQLite file |
| Workflow engine | The Hub's entry in the deployment manifest, then HUBZOID_DBOS_DB, then DATABASE_URL when it is PostgreSQL, then <hub>/.hubzoid/dbos.db |
| Hub database | DATABASE_URL, then <hub>/.hubzoid/hub.db |
When several Hubs run together, the manifest (<data-dir>/deployment.json) is authoritative. A bridge or operator command whose HUBZOID_OPERATIONAL_DB or HUBZOID_DBOS_DB disagrees with it stops with an error instead of writing to a second store.
SQLite or PostgreSQL
SQLite is the default and needs no setup. It suits one machine with one bridge per Hub, and hubzoid backup copies it consistently while the Hub runs.
Choose PostgreSQL for a managed database server with its own backups and point-in-time recovery, or for processes on more than one machine. Install the driver extra and use the postgresql+psycopg:// URL form, because Hubzoid installs the psycopg 3 driver only:
pip install "hubzoid[postgres]"Hubzoid upgrades its own tables (prefixed hz_) at start. The workflow engine keeps its tables in the dbos schema. Hubzoid does not copy an existing SQLite deployment into PostgreSQL, so start a PostgreSQL deployment fresh or move the data with your own tooling.
There are two ways to point Hubzoid at PostgreSQL:
| Set | Moves to PostgreSQL | Stays in SQLite | Use it for |
|---|---|---|---|
HUBZOID_OPERATIONAL_DB and HUBZOID_DBOS_DB | Operational store, workflow engine | Each Hub's .hubzoid/hub.db | Several Hubs, and any deployment where Hubs could share one database |
DATABASE_URL | Operational store, workflow engine, Hub database | Nothing | A single Hub |
For several Hubs, set both in the .env next to hubzoid.yaml. They can name the same database:
HUBZOID_OPERATIONAL_DB=postgresql+psycopg://hubzoid:<password>@db.internal:5432/hubzoid
HUBZOID_DBOS_DB=postgresql+psycopg://hubzoid:<password>@db.internal:5432/hubzoidhubzoid backup dumps each distinct PostgreSQL database with pg_dump into the same archive, so install the PostgreSQL client tools at the server's major version or newer. See backup and restore.
How sharing one database behaves
The choice between the two forms follows from what each store is keyed on. In short, the operational store is shared by design, and the Hub database is not.
What each store is keyed on
- The operational store is shared by design. Every row carries its Hub, so one store serves every Hub of a deployment. One account signs in to all of them, an organization grant made in one is visible to all, and one Settings page covers the deployment.
- The workflow engine can share PostgreSQL. Each Hub runs under its own application name and queues, so several Hubs can use one PostgreSQL system database. On SQLite each Hub keeps its own file.
- The Hub database is not keyed by Hub. It stores WhatsApp and Telegram history per sender (
whatsapp-<phone>,telegram-<id>). Hubs that share oneDATABASE_URLshare this table, so a person who writes to two Hubs over the same channel would get one merged history. TheHUBZOID_OPERATIONAL_DBandHUBZOID_DBOS_DBform keeps one Hub database per Hub.
Check any shape with hubzoid doctor <hub>. It reads the deployment without changing it and fails on misconfigured or unreachable databases (db.operational, db.hub).

