Tenancy
The SDK's tenancy surface answers one question: which organization operates this installation? The answer is a lightweight, database-free descriptor, the tenant, which SdkContext::tenant() carries through the request. Tenant isolation (keeping one customer's data away from another's) is not the SDK's job; that lives at the deployment boundary and belongs to the host.
The app is the tenant. Single-tenancy is the baseline, not a degenerate case. A standalone install is its own tenant and still owes the shell a real identity. Multi-tenancy is the same model with one extra upstream step: the host switches the database by request domain before resolution runs. The SDK never needs to know which mode it is in.
The SDK is a package you install to build an enterprise app on Laravel. Platform is the operational layer (a database per tenant routed by domain, customer onboarding, backup and restore) that turns it into a vertical SaaS, and we build it with you.
The model
Tenancy is four types:
| Type | Role |
|---|---|
TenantInterface | the operating organization's identity (name, logo, legal payload) |
TenantResolverInterface | resolves the tenant for the current request |
Tenant | the shipped DB-free POPO implementing TenantInterface |
MissingTenantResolverException | a bespoke resolver signalling it cannot resolve |
A Tenant is a curated org-identity subset of a Partner, with only the org-relevant fields:
interface TenantInterface
{
public function getId(): int|string; // domain- or partner-derived
public function getExtRef(): string;
public function getSystemLevel(): SystemLevel;
public function getName(): string; // display / legal name
public function getLogo(): string; // org Partner avatar_path (storage-relative)
public function getEmail(): string;
public function getPhone(): string;
public function getVatNumber(): string;
public function getRegistrationNumber(): string;
public function getRegisteredOffice(): string;
public function getFoundationDate(): ?Carbon;
}Person fields (first_name, gender, birthdate, …) are pruned. So is locale: it is an installation detail carried by the request (request()->getLocale()), not a property the tenant owns.
Tenant ≠ the actor's organizations
SdkContext distinguishes two org-shaped things, and they do not collide:
tenant(): who operates this installation (the operator company).orgPartners(): who the logged-in person represents (the actor's employer(s)).
They coincide when the operator dogfoods its own app. They diverge on a SaaS where a consultant from one company logs into an installation operated by the vendor.
The default resolver
Since SDK 1.2.0
Releases up to 1.1.1 ship a default that returns a fictional ACME operator and reads no database. On those releases, bind your own resolver (below) to see your organization.
Out of the box the SDK binds TenantResolverInterface to DefaultTenantResolver. It projects the installation's platform owner into a Tenant: the organization ui5:intake created, which is also the partner behind ui5.system_actor_id. The tenant of a fresh installation is therefore the organization you named in ui5:intake, with no further wiring.
TenantInterface | from the platform owner's sdk_partners column |
|---|---|
getId() | id |
getExtRef() | ext_ref |
getSystemLevel() | system_level (PlatformOwner) |
getName() | name |
getLogo() | avatar_path |
getEmail() | email |
getPhone() | phone |
getVatNumber() | vat_number |
getRegistrationNumber() | company_registration_number |
getRegisteredOffice() | registered_office |
getFoundationDate() | foundation_date |
A column the partner leaves empty comes through as an empty string. ui5:intake has no option for the logo, so getLogo() stays empty until the partner has an avatar. The partner's search_term and language_code are not part of the tenant. Without a platform owner, the resolver fails the same way ui5:sync does, with MissingSystemActorException. Run ui5:intake first.
A host that wants a different operator identity rebinds the resolver:
// AppServiceProvider::register()
$this->app->singleton(
\LaravelUi5\Sdk\Tenancy\Contracts\TenantResolverInterface::class,
\App\Tenancy\OperatingOrgResolver::class,
);In a multi-tenant deployment, the host's tenancy layer switches the database by domain upstream of this resolver. The resolver then reads the operating org from whatever database the request is already connected to. Selecting an operator across tenants is a Platform concern, not this surface.
MissingTenantResolverException is reserved for bespoke resolvers, such as a domain-based resolver in a multi-tenant host signalling an unresolvable tenant (an un-provisioned domain, a missing upstream DB switch). The shipped default does not use it.
Creating the operating organization: ui5:intake
php artisan ui5:intake creates the installation's platform-owner organization, the single SystemLevel::PlatformOwner partner of the database. It is the system actor: ui5:sync records it as set_by on every setting default it seeds. Run intake once per database, before the first sync:
php artisan migrate
php artisan ui5:intake
php artisan ui5:syncEvery property is an option (--name, --email, --vat, --office, …) and otherwise a prompt. There are no defaults: --name and --email are required, everything else stays empty unless you pass it. The command is idempotent and keyed on --email: a re-run updates the owner in place, and a second owner under a different email is refused.
The same partner is your tenant: the default resolver projects it.
Invariants
- Resolve once, immutable.
resolve()runs once per request, when the SDK builds the context for a module that requires authentication; the result lives in thereadonlySdkContext. There is no tenant switching mid-request: no setter, no re-resolve. A module that does not require authentication gets Core's context, which has no tenant. - Never a query dimension. The SDK adds no
tenant_idcolumn, ever. Isolation is per database (1 client = 1 DBplus a separate filesystem), never row-level. The tenant is derived within the already-isolated database, never selected across tenants. - The resolver is the only seam. The SDK depends on
TenantInterfaceandTenantResolverInterfaceand nothing else. The shipped default couples to one partner, the platform owner. A host resolver may couple to whatever it needs, but that coupling lives in the host. - Branding does not come from the tenant yet. The shell's sidebar name and logo come from a candidate chain, not from the tenant — so a multi-tenant host writes a candidate that resolves the tenant itself. The logo and the name beside it shows how. Tenant-driven branding is planned.
- A default always exists. Once
ui5:intakehas run, the platform owner is the tenant, so single-tenancy is served out of the box.
See also
- The System Actor: the platform owner and
ui5:intake - Architecture Overview: where the context is built