TechFabricTechFabricPlatform
Composable application standard

Identity and tenancy

Turning a credential into actor context, and why proving who someone is says nothing about where they may act.

Every governed action, projection decision, grant and audit record derives from actor context. It is the first thing to get right, and the most expensive to retrofit: change it after your integrations are built and you rebuild the integrations.

The platform does not authenticate

PlatformHost accepts actorId and actorType and trusts them. That is deliberate — it has no opinion about your identity provider — but it means something upstream must establish identity before the platform is called, and that something is yours.

IdentityPort is the seam. It is OAuth2/OIDC-shaped, so a gateway that already validates JWTs implements it without a second identity model.

const claims = await identity.verify(credential);
if (!claims) throw new Error("unauthenticated");

verify returns null for anything it cannot positively verify. It never returns partially trusted claims, and it never echoes credential material back — identityPortChecks asserts both.

Verification is half the job

A credential proves who the caller is. It does not prove they may act in the tenant and space this particular request names. A valid credential for one tenant, replayed against another, is a valid credential.

// Refuses expired, not-yet-valid, cross-tenant and out-of-space claims.
const actor = actorContextFromClaims(claims, { tenantId, spaceId });

Derive the submission fields from claims that passed this check rather than from request input. That is what keeps the audit trail tied to something that was actually proven.

Three details that decide whether this fails open

Space coverage is spelled out. spaceIds is either an explicit array or the literal "tenant-wide". There is no absent-means-everything case, because claims that simply forgot to carry coverage would otherwise read as covering everything.

Timestamps carry an offset. issuedAt and expiresAt must be RFC 3339 with an explicit offset. Date.parse accepts timezone-less strings and reads them in the host's local zone, which would make the same credential expire at different instants on different machines.

The clock is checked before it is trusted. Every comparison against an invalid clock is false, so an unvalidated now would let an expired credential through rather than reject it.

Actor kinds

Six, not two: natural_person, agent, system, service_account, external_system, integration. Policy treats them by capability rather than by kind, but a gateway design has to handle the last three differently from a human session, and they are easy to discover late.

A hierarchy of spaces

A claim carries one tenant and either a list of spaces or "tenant-wide". That is deliberately flat. A distributor with branches, each with dealers, each with customers, is a hierarchy the platform does not model, and does not need to: the identity adapter is where the hierarchy already lives, and expanding it into the claim is the adapter's job. A branch role becomes the enumerated spaces of the dealers assigned to that branch, refreshed when assignments change. A dealer user gets the dealer's space. An organization administrator gets "tenant-wide" only when the source entitlement says so. Missing coverage denies; it never implies the whole tenant.

Choose the space to be the unit that owns data and is authorized as a whole. For a distributor that is usually the dealer, with the organization as the tenant and branch identifiers carried as verified claims rather than as scope.

Proving isolation

Every other seam ships an executable definition of correct, and isolation was the guarantee every adopter tested for itself. tenantIsolationChecks() proves the kernel's part: a tenant's scope is a key on every read and write, never a filter somebody remembered to add. Six checks over a host and its store, with two scopes in different tenants: an invocation cannot be read, executed, or completed by callback under the other tenant's scope; an idempotency key is a tenant's own, so the same key in two tenants is two invocations; a tenant's event stream carries no other tenant's events; a worker scoped to one tenant never claims another's work; and, when the subject lends a governed read, a viewer in the other tenant sees none of ours. What the identity adapter derives into that scope is the identity port's suite; what a projection authorizes is the projection host's hook, which is why the last check is the subject's to lend.

What is still yours

Session management, token refresh, the login surface, and mapping your provider's claims onto ActorClaims. Fabric defines what verified identity looks like once it reaches the platform.

On this page