Table of Contents

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 reserved default tenant.
  • Tenant-Admin API (AddLatticeTenantAdminApi) adds the operator control-plane facade ILatticeTenantAdmin.

The program walks four acts:

  1. Tenant tree naming. A tenant-scoped tree id self-describes its owner: LatticeTenantTrees.Compose(tenant, name) yields t/{tenant}/{name}, and TryGetTenant reverses it. This structural prefix is what the isolation gate checks - a caller acting as tenant acme reaches only t/acme/* unless another tenant grants it access.
  2. 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 - acme is handed to an explicit delegated admin, while globex defaults to the creating operator, so a newly created tenant is always visible to someone.
  3. 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.
  4. Fail-closed control plane. The reserved default tenant 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