Identity), the tenant-scoped role or membership they are acting as (Principal), and the isolation boundary they operate within (Tenant). The same guards that power 2D mode support this shape without any driver changes — only your models and resolver wiring differ.
When an access token carries a
pid claim, the guard resolves exactly that principal and fails closed if it cannot. A token that claims pid = 42 but resolves to a different principal — or no principal at all — is rejected rather than silently downgraded. This prevents privilege-escalation across tenant boundaries.Create the Identity model
The identity represents the human account. It implements
Identity, HasDevices (for refresh-token rotation), and HasPrincipals (to expose the principals it may act as). Optionally implement ResolvesHintedPrincipal to short-circuit the default principals()->find($hint) lookup with a more efficient joined query.resolveDefaultPrincipal() is called when the token carries no pid hint — typically on first login before you embed the principal id into the token. resolveHintedPrincipal() is the preferred seam for optimising the hot bearer path: perform the joined lookup you need, attach the tenant relation you want Auth::tenant() to read, and set the inverse identity relation when appropriate.Create the Principal model
The principal represents the tenant-scoped actor — typically a membership or role row. It implements
Principal and belongs to a Tenant. The ActsAsPrincipal trait provides default getPrincipalIdentifier(), getIdentity(), getTenant(), and isActive() implementations sourced from conventional attribute and relation names (id, identity, tenant, is_active).ActsAsPrincipal reads is_active for the isActive() check and follows the tenant relation for getTenant(). Override getActiveAttributeName() or getTenantRelationName() if your schema uses different column or relation names.Create the Tenant model
The tenant is the isolation boundary the principal acts within. It implements
Tenant so Auth::tenant() can return it. Optionally implement HasType and use ProvidesTenantType to expose a categorical label (Auth::type()) — useful when a single app serves multiple audience types such as 'staff' and 'customer'.ProvidesTenantType reads the type column by default and resolves BackedEnum via value, UnitEnum via name, and any other value via string cast. Override getTenantTypeName() if your column is named differently. Drop HasType and ProvidesTenantType entirely if your app does not need tenant type discrimination.Register the guard in config/auth.php
The guard registration is identical to 2D mode. Point the provider at your identity model.
Understand what each accessor returns
Once a request carries a valid bearer token the guard rehydrates and binds all three layers. Each accessor reflects a distinct model:
Auth::tenant() follows the tenant relation on the resolved principal. If you pre-hydrate the relation inside resolveHintedPrincipal(), no additional query is issued.Custom PrincipalResolver
The package ships aDefaultPrincipalResolver that calls resolveDefaultPrincipal() for hint-free requests and resolveHintedPrincipal() (or principals()->find($hint)) when a pid is present. This covers most 3D apps.
When you need a domain-specific resolution strategy — subdomain-scoped tenancy, a custom request header, a session claim, or any logic that cannot live on the identity model — implement the PrincipalResolver contract and bind it in a service provider:
principal_resolver override in config/auth.php will use this binding for both bearer-token resolution and refresh exchange. Guards that declare their own resolver take precedence:
auth.guards.<name>.principal_resolver(per-guard config)- The app-wide
PrincipalResolver::classcontainer binding - The package default
DefaultPrincipalResolver