Multi-tenancy
This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at README.md, and llms.txt lists every page.An opt-in, single-silo tour of Orleans.Lattice multi-tenancy: the tenant registry, the isolation naming seam, and the operator control-plane facade - all turned on by adding a few packages, with no change to the core tree.
What it shows
One in-process Orleans silo runs the full control-plane stack:
- Membership (
AddLatticeMembership) resolves the ambient caller credential into a subject. - Auth (
AddLatticeAuth) installs the fail-closed enforcement gate. - Tenancy (
AddLatticeTenancy) turns on the tenant registry, the isolation seams, and the reserveddefaulttenant. - Tenant-Admin API (
AddLatticeTenantAdminApi) adds the operator control-plane facadeILatticeTenantAdmin.
The program walks four acts:
- Tenant tree naming. A tenant-scoped tree id self-describes its owner:
LatticeTenantTrees.Compose(tenant, name)yieldst/{tenant}/{name}, andTryGetTenantreverses it. This structural prefix is what the isolation gate checks - a caller acting as tenantacmereaches onlyt/acme/*unless another tenant grants it access. - Tenant lifecycle as a platform operator. A bootstrap administrator
creates two tenants and reads back their lifecycle status and the admin
subjects each was seeded with -
acmeis handed to an explicit delegated admin, whileglobexdefaults to the creating operator, so a newly created tenant is always visible to someone. - Lifecycle transitions and guards. Suspend / resume a tenant, delete a
tenant (cascading its trees), and prove create is not upsert - a second
create of the same id is refused with
TenantAlreadyExistsException. - Fail-closed control plane. The reserved
defaulttenant can never be deleted or suspended (ReservedTenantOperationException), and a caller who is not a platform operator is denied every lifecycle op (LatticeAuthorizationDeniedException) under the default-deny gate.
Run it
dotnet run --project samples/MultiTenancy
Expected tail:
== Act 4: fail-closed control plane ==
delete reserved 'default' -> refused (ReservedTenantOperationException)
create as non-operator 'mallory' -> denied (LatticeAuthorizationDeniedException)
[OK] tenant lifecycle ran end-to-end; the reserved tenant and the operator seam stayed fail-closed.
The process exits 0 on success and 1 if either Act 4 guard - the reserved-tenant
refusal or the non-operator denial - fails to hold. An unexpected result in Act 3 (for
example a re-create that is not refused) is printed but does not change the exit code.
How authorization works here
The gate runs default-deny (the production posture). Tenant-lifecycle
operations authorize Admin on the reserved authorization policy tree, which only a
bootstrap administrator - or a subject explicitly granted whole-tree Admin on that
tree through the access-administration delegation - holds, so the operator seam is
fail-closed against every other caller. A cluster-wide all-trees rule does not
reach that tree. The sample declares
platform-operator as a bootstrap administrator and runs each operator action
under that credential; the unrelated subject mallory is refused.
Where to go next
- Core tenancy concepts, isolation, quotas, metering, rate limiting, region residency, and observability: docs/lattice.tenancy.
- The operator control-plane facade surface: docs/lattice.api.tenantadmin.
- Driving the facade remotely over gRPC: docs/lattice.api.tenantadmin.grpc.