Multi-tenant operations
A tenant is a directory: one operator manifest plus its journals. Nothing
else. The runner (unidpp-stack tenant) reads the manifest, renders each
declared service’s environment through unidpp-config render-env, and starts
the same binaries the reference deployment runs. Zero per-tenant code.
tenants/├── acme/│ ├── unidpp-operator.yaml # whitelabel, EU│ ├── registry-journal.jsonl│ └── issuer-journal.jsonl└── acme-cn/ ├── unidpp-operator.yaml # sovereign, CN, sm2 packs ├── registry-journal.jsonl └── issuer-journal.jsonlTenant runtime state (pids, logs) lands under run/tenants/<name>/, one
directory per tenant.
The lifecycle
Section titled “The lifecycle”$ ./ops/target/release/unidpp-stack tenant acme uptenant acme (up): registry: started (pid 35999) issuer: started (pid 36003) console: started (pid 21035)
$ ./ops/target/release/unidpp-stack tenant acme statustenant acme (status): registry: running (pid 35999) issuer: running (pid 36003) console: running (pid 21035)
$ ./ops/target/release/unidpp-stack tenant acme downtenant acme (stop): stopped acme/registry stopped acme/issuer stopped acme/consoleup is idempotent: a service with a live pid file is reported
already running and left alone. down kills by pid file; journals are
never touched, so a stopped tenant restarts with its state replayed.
Upgrades
Section titled “Upgrades”An upgrade is new binaries over unchanged state. The compatibility contract
is the journal: every service replays its append-only JSONL on start, and
the manifest’s API version (unidpp.org/v1) is pinned, and a manifest the
running schema does not accept is a loud error, not a
silent-misconfiguration risk.
The procedure, per the runner’s own behavior:
- Stop:
unidpp-stack tenant <name> down(orunidpp-stack downfor the reference deployment). Journals are preserved by both; nothing is wiped. - Build the new binaries:
cargo build --releaseper repository.unidpp-stackrebuilds only when a binary is missing; force a rebuild of a present-but-stale tree withUNIDPP_FORCE_BUILD=1 unidpp-stack up. - Start: the same start command; every service replays its journal
(
unidpp-stack statusshows the journal record and item counts). - Verify: same acceptance as a restore: the registry serves the same item count as before the upgrade, and the log verifies its head (a tree size that moved backwards is a hard fault).
Between stop and start there is no migration step to forget: if the new binary can read the journal, the deployment is up; if it cannot, it says so at replay, before serving. Take a backup first when the jump is large, because the restore procedure is the rollback.
The console needs one variable the manifest cannot express (its own manifest
path), which the runner supplies: UNIDPP_CONSOLE_MANIFEST=tenants/<name>/unidpp-operator.yaml.
What a tenant declares
Section titled “What a tenant declares”A minimal tenant manifest declares registry + issuer + console, which is enough to issue passports, mint packs, and manage itself:
$ unidpp-config validate tenants/acme/unidpp-operator.yamlvalid: acme-eu (profile whitelabel, 3 service(s): ["registry", "issuer", "console"])
$ unidpp-config render-env issuer tenants/acme-cn/unidpp-operator.yamlUNIDPP_ISSUER_BIND=127.0.0.1:9593UNIDPP_ISSUER_PACK_SUITE=sm2UNIDPP_ISSUER_STATE_FILE=tenants/acme-cn/issuer-journal.jsonlA tenant can declare any subset of the family’s services; trust, log, projector, gateway, and archive blocks are optional. What a tenant does not declare, it does not run.
Isolation
Section titled “Isolation”What separates tenants from each other and from the reference deployment:
- Journals. Each service’s
state_filepoints inside the tenant directory. State never crosses tenants. - Ports. Each tenant’s binds are its own (acme on 93xx, acme-cn on 95xx). The runner does not invent ports; the manifest declares them and a collision is a manifest fix.
- Keys. Each issuer derives its keyring from its seed environment. Two tenants’ packs verify against two different published anchors. (Dev seeds are shared by default; see security posture; production tenants set per-tenant seeds.)
- Branding and policy. Per-tenant manifest: names, theme, footer, pack suites, residency, egress.
What is not isolated: the binaries (shared, by design), the host, and the process table — every tenant is a process group on your box; treat host access accordingly.
Port allocation
Section titled “Port allocation”The family’s working convention:
| Range | User |
|---|---|
| 8389-8397 | reference deployment (console, the seven core services, and the hub on 8397) |
| 8399 | JP national peer registry |
| 93xx | whitelabel tenants (acme: 9389 console, 9390 registry, 9393 issuer) |
| 95xx | sovereign tenants (acme-cn: 9589 console, 9590 registry, 9593 issuer) |
There is no registry of ports beyond the manifests themselves; when adding a tenant, pick a decade and stay in it.
Operating against a tenant
Section titled “Operating against a tenant”Everything in the service references applies to a tenant’s services, with the same endpoints and shapes on the tenant bind. Example: the whitelabel tenant’s issuer answers on 9393 exactly as the reference issuer answers on 8393:
$ curl -s http://127.0.0.1:9393/keyring | jq '{mode, roles: (.roles | keys)}'{ "mode": "seeded-dev", "roles": [ "event", "pack" ]}And the CN tenant’s issuer would answer the same query on 9593 with an SM2 pack role. Issuing a passport on a tenant is the first-passport walkthrough with the bind changed.
Adding a tenant
Section titled “Adding a tenant”mkdir tenants/<name>and writeunidpp-operator.yaml(start fromtenants/acme/, the closest profile).unidpp-config validate tenants/<name>/unidpp-operator.yamluntil it passes../ops/target/release/unidpp-stack tenant <name> up.- Probe:
curl -s http://127.0.0.1:<console-port>/.well-known/unidpp-service.
The full procedure with branding, tokens, and tunnel is the 15-minute whitelabel deployment.
The lifecycle’s end: decommission
Section titled “The lifecycle’s end: decommission”Retiring a tenant is the doctrine-shaped act: prove, then move aside, never delete:
./unidpp-ops decommission northwind# stopped (if running) -> a final VERIFIED data-only bundle# -> tenants/northwind -> tenants/northwind.retired.<stamp># reversal: mv back && ./ops/target/release/unidpp-stack tenant northwind upThe bundle is the audit artifact; the rename-aside is the reversal path; nothing is ever destroyed.
Taking a tenant on-prem: the bundle
Section titled “Taking a tenant on-prem: the bundle”Whitelabel as a separate deployment ships as one verified artifact:
./unidpp-ops bundle acme-cnThe bundle carries the tenant’s manifest (secrets stay ${VAR}
references; render-env substitutes them at the target host), the
release binaries for exactly the services the manifest declares, its
own runner (unidpp-run, with binaries from ./bin and manifest at the
bundle root), an .env template, and a runbook. Its sidecar lists
every member’s SHA-256, and a bundle verifies exactly like a backup
(./unidpp-ops verify <bundle>.tar.gz), so the receiving host
proves integrity independently of its producer. First boot on the
target: ./bin/unidpp-config validate, fill .env, ./unidpp-run start; the bundle starts from its own contents (CI smokes exactly
this).