Documentation > Deployment > Multi-Tenancy

Deploying Multi-Tenancy

Standing up a multi-tenant SysManage deployment — the databases, OpenBAO, the migration chains, and tenant provisioning — for development and production.

Before you start

Multi-tenancy is the Enterprise SaaS deployment model: a control plane plus an isolated database per tenant. It is opt-in and licensed — with it off, SysManage installs exactly as the single-tenant guide describes (Server Installation). Read the architecture first so the pieces below make sense.

What you will set up

A multi-tenant deployment introduces databases beyond the single application database of a normal install:

DatabasePartitionPurpose
Registry / bootstrapregistry_*The tenant catalog and per-tenant database placements. This is the database sysmanage.yaml points at.
Sharedshared_*Canonical reference data identical for every tenant (collapses onto the bootstrap database until the dedicated shared engine is configured).
Tenant (one per tenant)unprefixedEach tenant's host-scoped operational data, in its own database.

Three things make this work: PostgreSQL (one or more instances), OpenBAO (to broker per-tenant database credentials), and the licensed multitenancy engine (the routing logic). Without the engine loaded, a deployment configured for multi-tenancy fails loudly with a licensing error rather than misrouting.

Development setup

The fastest way to develop against multi-tenancy locally. By default a dev checkout runs collapsed — multi-tenancy off, all three migration chains applied to one database — which is the normal single-tenant experience. Turn it on explicitly:

  1. Install the dev environment and PostgreSQL as in the Server Installation guide (clone, make install-dev, create the sysmanage role and database).
  2. Enable multi-tenancy in sysmanage-dev.yaml. Point the bootstrap/registry block at your registry database, turn the feature on, and configure the local OpenBAO (the dev config already ships a vault: block):
    registry:
      host: localhost
      port: 5432
      name: sysmanage_registry
      user: sysmanage
      password: DEV_Change_Me_ABC123!
    
    multitenancy:
      enabled: true
      self_service_provisioning: true
    
    vault:
      # OpenBAO brokers per-tenant database credentials
      url: http://localhost:8200
      bootstrap:
        config_file: ./openbao.hcl
        data_path: ./data/openbao
  3. Run the migrations. make migrate drives scripts/sysmanage_migrate.py — the same tool operators run in production. With multi-tenancy on it automatically starts OpenBAO for the per-tenant fan-out, applies the registry, shared, and tenant chains, then stops OpenBAO again:
    make migrate
  4. Provision the bootstrap provisioner identity — a scoped Postgres role plus OpenBAO policy so the server can create tenants without holding superuser:
    make provision-bootstrap ARGS='--bao-token $BAO_TOKEN'
  5. Start the stack and create a tenant. make start, then create a tenant from the control-plane UI (Settings → Tenants). With self_service_provisioning on, the server provisions the tenant's database and OpenBAO credentials for you.
  6. Fan the tenant chain out to the newly created tenant database(s):
    make migrate-tenants
Want the full distributed topology? The server repository ships scripts/buildMultiTenantTestNetwork.sh, which provisions separate VMs for the registry, shared, and per-tenant databases plus a control plane — a faithful multi-database environment for exercising real routing instead of the collapsed single-database dev default.

Production setup

Production follows the same shape, with real databases, a hardened OpenBAO, and least-privilege provisioning.

  1. Provision the registry database and a login role. On your PostgreSQL instance, create the sysmanage role and the registry database it owns (see the Server Installation database step). The server connects as this role; it is not a superuser.
  2. Stand up OpenBAO and configure its database secrets engine so it can issue short-lived, per-tenant PostgreSQL credentials. OpenBAO is the broker that keeps the server from ever holding a long-lived, shared database password. (Build and start it with the repository's scripts/build-openbao.sh and scripts/start-openbao.sh, or your own managed instance.)
  3. Configure /etc/sysmanage.yaml — point the registry block at the registry database, enable multi-tenancy, and configure the vault:
    registry:
      host: db-registry.internal
      port: 5432
      name: sysmanage_registry
      user: sysmanage
      password: <from your secret store>
    
    multitenancy:
      enabled: true
      self_service_provisioning: true
    
    vault:
      url: https://openbao.internal:8200
      # token / auth configured per your OpenBAO setup
    Licensing: per-tenant routing requires the licensed multitenancy engine (the MULTITENANT_SAAS tier). Without it, the server refuses to run multi-tenant rather than silently misrouting — this is intentional.
  4. Run the migrations with the operator tool. make migrate (or scripts/sysmanage_migrate.py directly) applies the registry, shared, and tenant chains and shows per-database progress; with multi-tenancy on it brings OpenBAO up for the fan-out:
    make migrate
  5. Provision the least-privilege provisioner identity so self-service tenant creation works without cluster-superuser rights:
    make provision-bootstrap ARGS='--bao-token $BAO_TOKEN'
  6. Create your tenants from the control-plane UI (Settings → Tenants). Each tenant gets its own database and its own OpenBAO-brokered credentials. Place each tenant's database where it needs to live — by host, region, or jurisdiction.
  7. Fan the tenant chain out to every provisioned tenant database (OpenBAO must be running):
    make migrate-tenants
  8. Bind hosts to tenants. Issue a tenant-scoped enrollment token per tenant (Tenants → token), drop it into that host's agent configuration under security.enrollment_token, and register the agent. From then on the host's data lands in its tenant's database automatically.

Migrations in production

Schema changes use an expand-contract discipline so a migration is always safe to apply while the previous version of the server is still running. Each partition (registry, shared, tenant) has its own linear, idempotent Alembic chain with a separate version table, and the operator tool applies them across the bootstrap database and every tenant database, reporting progress. The same sysmanage_migrate.py you run in development is what runs in production — there is no separate, riskier production path.

Verify routing

Confirm a bound host's data really lands in its tenant database rather than the bootstrap one — switch the active tenant in the UI and compare host counts, or query the tenant databases directly:

psql 'postgresql://sysmanage@db-tenant-a:5432/tenant_a' -c 'SELECT count(*) FROM hosts;'
psql 'postgresql://sysmanage@db-tenant-b:5432/tenant_b' -c 'SELECT count(*) FROM hosts;'

Each tenant's hosts appear only in that tenant's database. If the router cannot resolve a tenant for a piece of work, it logs loudly with full context rather than guessing — check the server logs.

Related documentation

🏗️ Architecture

The control plane, partitions, routing seam, and topology diagram.

Multi-tenancy architecture →

🔐 Security

Why per-database isolation and OpenBAO credentials are hard to attack.

Multi-tenant security →

🖥️ Server Installation

The base single-tenant install: prerequisites, database setup, and configuration.

Server installation →