Opbox is deployed one box per client. A tenant is not a row in a shared multi-tenant database that everyone’s data lives in together; it is its own self-contained stack - its own Postgres, its own kernel, its own secrets, its own front door. This page explains that model and walks the practical steps to stand up a new tenant, run several walled-off clients on one box if you must, and decommission cleanly.
The model: one client = one box = one workspace
The deployment unit is a single docker-compose stack (see The stack). One client gets one box, and on that box one workspace - the wall around that client’s matters, documents, files, bills, and parties.
Isolation comes from two independent fences, not one:
- The box. Each tenant’s data plane (
postgres,docrender) lives on aninternalDocker network with no host port and no egress. Only Caddy is internet-facing. A different client is a different box entirely, so there is no shared database to leak across in the first place. This is the strongest, simplest fence: physical separation. - The workspace fence inside Postgres (RLS). Even on a single box, every row is scoped to a workspace and
guarded by Postgres row-level security. Each kernel transaction stamps
app.workspace_idand switches to the unprivilegedopbox_approle (migration0010); the RLS policies then physically refuse any row outside that workspace. It fails closed: a transaction with no workspace stamped matches no rows, never all of them. This is the backstop the Security page calls Lock 1 - the database wall that holds even when the application code is wrong.
So the box is the outer fence and RLS is the inner one. You get defence in depth without ever relying on a shared-tenant query filter being correct in every code path.
Organisation vs workspace
A workspace is one client’s wall. An organisation is the layer above it: it can hold several
workspaces (a firm running more than one practice area, each walled off). Organisation membership and
workspace membership are separate grants - an org OVERSEER can read the audit trail across the org’s
workspaces, but that is a deliberate, recorded oversight power, not a dissolving of the walls. See
Security for the full treatment. For most single-client boxes you have exactly one
org and one workspace and never think about the distinction.
Provisioning a new tenant
This is the honest sequence. Most of it is one host script; the parts that are manual are called out.
1. Fill the secrets
Copy the example env and fill every required secret. The compose stack uses the ${VAR:?} form, so a
missing secret fails the bring-up immediately rather than booting on a dev default.
cp .env.example .env # generation commands are in the file
The required secrets (POSTGRES_PASSWORD, OPBOX_PII_KEY, DOC_RENDER_HMAC_SECRET, OPBOX_WEBHOOK_KEY,
OPBOX_SITE_ADDRESS, an LLM key) are documented on The stack. Leave OPBOX_AGENT_KEY
blank - genesis mints it.
2. Bring up the stack and run migrations
Migrations are automatic. The kernel runs db::migrate on startup for serve (and for initiate and
mcp), so a clean stack migrates itself to the latest schema the moment it boots - you do not normally run a
separate migration step. (An explicit opbox migrate exists if you ever want to apply migrations and exit
without serving.)
3. Genesis - the founding ceremony
A release kernel drops its dev identity (ADR-0040), so a fresh box has the law loaded but zero principals.
Genesis is the one deliberate host act that founds the first ones. The genesis.sh
wrapper runs the whole sequence:
bash genesis.sh
What it actually does, in order:
- Brings the stack up (
docker compose -f docker-compose.yml -f docker-compose.beelink.yml --env-file .env up -d) and waits for the kernel/health. - Runs
opbox initiate, which creates the workspace and the founding OWNER actor and mints a standing root bearer. The bearer is printed once to stdout - capture it; only its hash is stored, so it is unrecoverable after. This is your admin credential.initiateis fail-closed and one-shot: it refuses if the workspace already has an active OWNER, so re-running genesis is safe. - Mints the Chief of Staff agent (autonomy 3, full scope) as the owner, then has the CoS mint the
bounded WORKER chat-agent (autonomy 1, MEMBER tier) that the public chat actually runs under. The
WORKER key is written to
.envasOPBOX_AGENT_KEYandagent-chatis recreated with it. The public chat never runs as the OWNER CoS key - that is the no-amplification rule. See Genesis for why the CoS-then-WORKER order is required.
After genesis, every other person is seated in-app (via actor.invite / actor.setRole) within the
granted authority - never by re-running this ceremony.
4. Verify and seed config
# confirm the release dropped dev-identity (a no-bearer call must return 401):
docker exec Opbox-Kernel sh -lc 'scripts/verify-prod-binary.sh http://127.0.0.1:8088'
To enable email + password sign-in, the owner sets their own credentials once via auth.credentials.set
(password policy: 10+ chars, two character classes); after that POST /auth/login returns the SESSION token
the chat signs in with.
Seeding the tenant’s vertical (templates, knowledge, policies, verb-packs) is a normal in-app step over the
kernel verbs - the setup.* lifecycle (setup.define / publish / apply / adopt / diff) authors and
applies a configuration to the workspace. A vertical is configuration, not a gated tier, so seeding a
vertical is just authoring its config on this box; see Building a vertical.
Honest note on the
tenant.*verbs.tenant.deploy,tenant.upgrade, andtenant.deprovisionare governed record-keeping seams, not the orchestrator. They write an audited ops-event for the lifecycle transition and returnSTUB_OFFBOX; the real image build/roll and the real teardown are the operator-script edge. Provisioning today is the host steps above (env + compose + genesis), not a one-click fleettenant.deploy. The fleet automation (zero-downtime cutover, rollback orchestration, the provisioning state machine, the OpsConsole progress UI) is specified but deferred - treat tenant onboarding as a deliberate, auditable host operation.
Running several workspaces on one box
The one-box-per-client default keeps the blast radius smallest, but the architecture does not require it. Because everything is workspace-scoped under RLS, more than one client can safely share a single box’s Postgres: each workspace’s rows are walled off by the same database wall described above, and the encryption-at-rest binding ties each encrypted blob to its own row + workspace (a blob lifted into another workspace simply fails to decrypt - Lock 6 on the Security page).
To add a second workspace, found it with its own owner - initiate takes the workspace id, and it is one-shot
per workspace, so a second call with a new id seats that workspace’s first OWNER without disturbing the
first:
docker exec Opbox-Kernel opbox initiate \
--owner-name "Second Client Owner" --workspace-id ws_second --workspace-name "Second Client"
When to do this: a firm with several internal practice areas (one org, many workspaces) is the natural fit - shared infrastructure, walled data, org-level oversight. When not to: genuinely separate clients with distinct regulatory or confidentiality boundaries are better off on their own box, where the physical fence backs the RLS fence and a single-box compromise or operator error cannot touch the other client at all. RLS scales the multi-workspace case correctly; the box boundary is still the strongest isolation you can offer, so reserve a shared box for workspaces that genuinely belong to the same trust domain.
Decommissioning a tenant
Tearing a tenant down is the reverse of standing it up, and it is operator-driven, not a single verb.
-
Record the governed request. Call
tenant.deprovision(OWNER + MFA step-up; it is a destructive, owner-tier verb) with a reason and thepurgemode you intend (ARCHIVEvsPURGE). This writes the audited lifecycle event so the teardown is on the permanent record before anything is destroyed. The verb itself returnsSTUB_OFFBOX- it records the intent; it does not delete the data. -
Take the final backup / archive of
opbox_pgdata(and any file storage) off-box first. The verb does not do this for you; the off-box archive write is the flagged follow-on. -
Tear the stack down on the host:
# stop and remove the stack, KEEPING the volumes (reversible): docker compose -f docker-compose.yml -f docker-compose.beelink.yml --env-file .env down # destroy the data too (irreversible - only after the archive is confirmed off-box): docker compose -f docker-compose.yml -f docker-compose.beelink.yml --env-file .env down -v -
Rotate / revoke any external credentials the tenant held (LLM key, webhook keys, the public hostname / DNS), and remove the box from your DNS and any fleet inventory.
For a multi-workspace box where only one workspace is leaving, you do not tear down the box. Archive and remove that workspace’s data through the governed verbs and leave the others running - the RLS wall means the departing workspace’s removal never touches the rest.
See also
- Security & permissions - the seven locks, with the workspace RLS fence as Lock 1 and org-vs-workspace membership explained in full.
- Genesis - the founding ceremony in detail (owner bearer, CoS, the bounded chat-agent).
- The stack - the services, the required secrets, and bringing the stack up.