Multi-tenant operations
A tenant is a directory: one operator manifest plus its journals. Nothing
else. The launcher (tenants/up.sh) 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/├── up.sh # the launcher: <name> [start|stop|status]├── 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”$ ./tenants/up.sh acme starttenant acme: registry: started (pid 35999) issuer: started (pid 36003) console: started (pid 21035)
$ ./tenants/up.sh acme statustenant acme (status): registry: running (pid 35999) issuer: running (pid 36003) console: running (pid 21035)
$ ./tenants/up.sh acme stoptenant acme (stop): stopped acme/registry stopped acme/issuer stopped acme/consolestart is idempotent: a service with a live pid file is reported
already running and left alone. stop kills by pid file; journals are
never touched — 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 — a launcher or
manifest the running schema does not accept is a loud error, not a
silent-misconfiguration risk.
The procedure, per the launcher’s own behavior:
- Stop —
./tenants/up.sh <name> stop(or./stack.sh stopfor the reference deployment). Journals are preserved by both; nothing is wiped. - Build the new binaries —
cargo build --releaseper repository.stack.shrebuilds only when a binary is missing; force a rebuild of a present-but-stale tree withUNIDPP_FORCE_BUILD=1 ./stack.sh start. - Start — the same start command; every service replays its journal
(
stack.sh 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
(
GET /tree/head— tree heads are monotonic by construction; a head 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 — the restore procedure is the rollback.
The console needs one variable the manifest cannot express (its own manifest
path), which up.sh 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 — 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 launcher 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 —
unless you say otherwise in sovereignty — the egress boundary. A 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-8396 | reference deployment (console + seven services) |
| 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 — same endpoints, same shapes, 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../tenants/up.sh <name> start.- 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.