Broadcasting Tenancy
Broadcasting (websockets) is one of the few things the package doesn’t automatically solve out of the box. This is due to multi-tenant broadcasting being highly setup-specific.
That said, Tenancy comes with two approaches for making broadcasting tenant-aware:
- tenant-specific broadcasting config (e.g. tenant-specific Pusher keys)
- prefixed channel names
The former requires fewer code changes, but your tenants will need to bring their own keys (at least in case of commercial services like Pusher). When using self-hosted broadcasting solutions, like soketi, you might be able to simply assign different keys to different tenants even if they ultimately all connect to the same server.
The latter requires more changes, but works with pretty much any broadcasting setup. It lets you use a single set of API keys and instead dynamically create channels for tenants (e.g. feed
becomes {tenantId}.feed
).
Making the broadcasting auth route tenant-aware
Apply the relevant tenancy middleware (universal
and your tenant identification middleware, or tenant
if you’re using early identification) on your broadcasting auth routes:
Tenant-specific broadcasting config
This approach lets you use a different broadcasting config (= API keys) for each tenant. As mentioned above, the benefit is that this is easy to set up, but the drawbacks are:
- your tenants having to bring their own API keys (in the case of Pusher, with other solutions this might be easier)
- not being able to send messages between tenant applications and the central application. Once tenancy is initialized, Laravel’s broadcasting logic will be fully switched to the new broadcasting config, which will differ from the config used in the central application
To use this approach, enable the respective bootstrapper:
The BroadcastingConfigBootstrapper
maps tenant attributes to the broadcasting config so that broadcasters can use attributes of the current tenant as credentials.
The bootstrapper comes with map presets for pusher
, reverb
, and ably
:
These are used based on the broadcaster being used. Meaning, if you’re using Pusher, tenant properties will be mapped like this:
$tenant->pusher_key
=>broadcasting.connections.pusher.key
$tenant->pusher_secret
=>broadcasting.connections.pusher.secret
You’re free to use your own config instead of these presets.
For example, to map the pusher_key
tenant attribute ($tenant->pusher_key
) to the Pusher broadcaster’s API key, you could do this:
On top of modifying config, the bootstrapper also overrides the BroadcastManager
singleton with an instance of TenancyBroadcastManager
, which refreshes broadcasters specified in the $tenantBroadcasters
(‘pusher’, ‘reverb’, and ‘ably’ by default, feel free to change that) so that the current credentials are used.
The last step to making this work is to update your Laravel Echo configuration. Since the broadcasting configuration will be tenant-specific, we need to set these dynamically and can’t have hardcoded values as part of a frontend bundle.
If you can instantiate Echo directly in Blade, you could use:
Alternatively, put the dynamic values into some global object that’s initialized before Echo will be instantiated:
And then your frontend bundle can use the value:
Prefixed channel names
This approach introduces the following concepts:
- central channels (the ones you’re already using in your central app)
- tenant channels (scoped to individual tenants)
- universal channels (a channel that works the same way in the central context and in tenant contexts, but data is scoped to the central context/individual tenant)
- global channels (messages are shared across all tenants and the central context — all contexts interact with the same channel)
The different channel registration is the first part of this approach.
The second part is tenancy automatically scoping broadcast messages — so that they use the right channel, given the current context.
The third part is integrating this with Echo.
Channel types
For Tenancy to accurately determine the types of your channels, they must follow our naming convention:
- Central channels don’t require any prefix. Events broadcast on central channels will only be available in the central app
- Tenant channels have to be prefixed with the tenant prefix (
{tenant}.
— e.g.{tenant}.channel
). Events broadcast on tenant channels will be contained within that tenant - Universal channels is a term for a central channel that has a corresponding tenant channel (e.g.
channel
and{tenant}.channel
). So a universal channel is not a single channel, but rather two separate channels: a central one without a prefix, and a tenant one with a prefix. Tenancy will determine which channel to use depending on the current context - Global channels are channels available in both the central and tenant apps. They have to be prefixed with the
global__
prefix (e.g.global__channel
). They’re similar to universal channels, except a global channel is just a single channel usable in any context, whereas a universal channel is separate, scoped channels with identical behavior.
Channel registration helpers
Tenancy provides helper functions for some of the mentioned channel types to make registering them more convenient.
Central channels are registered in the same way as in a single-tenant application: using Broadcast::channel(...)
. Central channels can’t use prefixes reserved for the other channel types.
The tenant_channel()
helper registers a channel with the tenant prefix. The tenant channel closure accepts the tenant key (from the {tenant}
parameter), but that’s solely for the purposes of name prefixing.
The global_channel()
method registers a global channel.
For example, this:
is equivalent to this:
Please note the unused $tenant
parameter. It is necessary if you want to include additional arguments, like $product
in this case.
To register a universal channel, you may use both global_channel()
and tenant_channel()
with the same definition.
The only difference is that the tenant channel needs to have the $tenant
parameter as mentioned above:
Universal channels unfortunately don’t have a helper for easy registration, due to limitations of PHP’s reflection (same reason as why the $tenant
parameter is needed).
BroadcastChannelPrefixBootstrapper
After your channels are defined as central, tenant, universal, or global, you need to enable the bootstrapper that will scope broadcast messages to these channels on the backend:
Without this bootstrapper, you’d have to manually determine which channel should be used in the broadcastOn()
method of your events:
With this bootstrapper, tenancy does this automatically for you in the background:
The way the bootstrapper works is that it overrides individual broadcasters using anonymous classes that override the formatChannels()
method. This lets the package run additional logic to determine how the channel from the event (orders
in our example) should be formatted.
Out of the box, the bootstrapper only provides overrides for Pusher, Reverb, and Ably. These can be enabled using their respective methods:
Only enable the one you’re using.
These methods register an override in the $broadcasterOverrides
static property. To add your own override, you could use:
So far, we have completed two of the three steps: we have created separate channels for the central part of the application and the tenant part of the application, and we have enabled the BroadcastChannelPrefixBootstrapper
, along with the right override (pusher()
, reverb()
, or ably()
).
The final step is making your frontend listen to these prefixed channels.
Making Echo channels tenant-aware
Start by sharing the current tenant ID with your frontend. If you’re using backend-rendered templates, you can simply add this to your Blade layout:
Next, use this variable in your frontend bundle to create the tenant channel prefix:
Now there are two ways you can use this prefix. You can either add it to your channel names manually:
Or you can register your channels upfront, and once all channels are registered, you can hook into the window.Echo
object to clone all channels and create their tenant counterpart (even though for some it might not exist on the backend — this can’t be determined in JavaScript, but it’s fine since the channel will simply never be referenced assuming your frontend application logic is correct):