This article is the first in a series that focuses on building a B2B SaaS application and is based on a B2B SaaS project that I'm currently working on.
Core B2B SaaS topics typically include things like Membership, Subscriber (a.k.a. Delegated) Administration and Invite workflow, and in this article covering Multi-Tenancy, I'm going to focus on how to implement a multi-tenant solution leveraging the Auth0 platform and the Auth0 Organizations feature in particular 😎
If you're completely new to B2B SaaS, then I'd highly recommend you first start by checking out my B2B SaaS Application Primer. That article gives you a gentle lead-in to the key concepts involved in building a B2B SaaS application and also introduces Auth0.
If you're building a B2B SaaS application but haven't come across the term Multi-Tenancy, then this article is definitely for you! If you have come across the term Multi-Tenancy but have never heard of Auth0 or the Auth0 Organizations feature, then you too are absolutely in the right place! And even if you have heard of all of those things, this article is still worth a read: you never know, you might just find something that surprises you, and you might just end up learning something new in the process 🤗
What is Multi-Tenancy?
If the term "tenancy" conjures an image of an apartment block full of apartments that folks occupy, then you wouldn't be a million miles away from what the concept relates to in a B2B SaaS context. Tenancy - or, more specifically, the term Multi-Tenancy - is the notion where more than one customer occupies space within your B2B SaaS application. Let me elaborate.
Take the image associated with this article. That image - as in the graphic that forms part of the title of this blog post - shows a cross-section of an apartment building that has multiple apartments. The apartments that we can see are all occupied, with each apartment decorated and furnished in a style unique to each occupant; each apartment is a home where the layout of the contents and the color scheme associated with each largely align with individual tastes and requirements. We can see that each apartment has physical characteristics - i.e. shape, size, etc. - that are defined by the apartment building itself, and we can imagine that each apartment has running water, heating, and electricity that's provided as part of the fabric of the said building.
A B2B SaaS application is similar. Like the apartment building in the previous analogy, a B2B SaaS application will typically provide the application fabric which each tenant will share and upon which each will rely. Similarly, per-tenant data and branding (discussed in more detail later) are analogous to the individual contents and the color scheme associated with each apartment in the apartment block.
The notion of B2B SaaS brings more "players" into the game, and with the various B2B2C, B2B2B and B2B2E variations (namely Business-to-Business to Consumer, Business-to-Business to Business, and Business-to-Business to Employee) the use of the terms "customer", "consumer", and "user" can quickly become overused...not to say confusing! 😳 So, when referring to the immediate customer - i.e. the occupant of a tenant/tenancy holder in a B2B SaaS application scenario - I typically prefer to use the term Subscriber. This is also an apt term, as B2B SaaS often employs a subscription-based model when it comes to the provision and the billing of the services provided. The various B2B variations and the best billing model to use when it comes to a SaaS subscription are beyond the scope of this article, but throughout, I have made a few comments regarding the decisions I've made in TheatricalPA - the B2B SaaS application I'm building - that may be useful.
In contrast, I typically use the term Vendor when referring to the provider of the B2B SaaS application (as in the supervisor/owner of the building in the apartment block analogy). The term Vendor is a good counterpoint to the term Subscriber and also provides useful delineation; for example, when it comes to describing the difference between the two administration scenarios, each Subscriber is typically kept separate intentionally, whilst a Vendor would likely be someone a Subscriber would want to share access with, given the appropriate consent. Vendor Administration vs Subscriber Administration for B2B SaaS applications is the topic of another blog post I'm working on, so stay tuned for more details! 🤗
Creating a multi-tenant architecture then involves several different aspects, each of which you'll almost certainly need to consider when building your B2B SaaS application. Much like in the apartment block analogy, there's typically no "one size fits all" design - apartment blocks come in all shapes and sizes, as do the apartments themselves, and so the "shape" of your application rather depends on the features/functionality that it is intended to provide.
Multi-Tenant Organization
Using the term "organization" is apt as it also refers to the feature in Auth0 - namely the Auth0 Organizations feature - that relates to multi-tenancy in an IAM (Identity and Access Management) context, and it builds upon the plethora of existing functionality provided by the Auth0 platform. More on this later 😁
When it comes to B2B SaaS, a Multi-Tenant "organization" typically falls into one of two camps: Sub-domain Organization and Sub-folder Organization. There is a use case for using both sub-domain and sub-folder organizations, but this isn't typically tenant-related, so we won't dwell on it as part of this article. If I were to draw a parallel to our apartment analogy, then one could consider "organization" as the strategy used when it comes to how an apartment is located within the apartment block.
Sub-domain Organization
The sub-domain Organization model is one where a tenant is represented as the sub-domain in the URL associated with a B2B SaaS application. For example, consider
auth0.auth0.com
. Here, auth0.com
is the domain associated with the application - i.e. Auth0 by Okta; itself a B2B SaaS application - and the sub-domain refers to the subscriber who has a provisioned tenant (more about tenant provisioning later). In this case, the subscriber is auth0
: yes, as part of the Okta family, the Auth0 team uses Okta identity products internally 🎉 Organization by sub-domain is typically best suited for isolated partitioning when it comes to things like SEO - drawing again on the analogy, think of maybe apartment(s) that form isolated, individual, perhaps gated, communities - and for a number of reasons also provides additional safety from an IAM perspective. For this reason, the Auth0 Organizations feature supports the use of sub-domain organization as the only configuration option.
Sub-folder Organization
The opposite of Sub-domain Organization is Sub-folder Organization. In a sub-folder organization model, each tenant is represented as a (sub) folder in the URL associated with a B2B SaaS application. Take
github.com/auth0
for instance. Here, github.com
is the domain associated with the application - i.e. GitHub; again, being a B2B SaaS application - and the sub-folder refers to the subscriber for whom a tenant has been provisioned...in this case, auth0
. A sub-folder organization model is typically best suited, for example, when it comes to inclusive search for likes of SEO, and one can perhaps think of it as being similar to, say, an apartment directory in the apartment block analogy where searching across tenancy is all-inclusive. It's often a model that can be used when creating something like an inter-related blog architecture - e.g. in something like a classic WordPress Multisite scenario - but it isn't as robust from an IAM perspective (and hence why it's currently not supported in the Auth0 Organizations feature). FYI, I'll be talking more about WordPress Multisite later, as it's the technology stack framework/scaffolding that I'm using in the B2B SaaS application (TheatricalPA) that I'm building 😎
Multi-Tenant Isolation
Isolation is all about how the various subscribers in a B2B SaaS application are kept separate from one another. Think about the apartment block analogy: as a tenant in one apartment, I wouldn't want any of the other tenants to come mess with my stuff, layout, and/or decor; I wouldn't want the occupants of any other apartment to just be able to come into mine. However, I might need the building supervisor, say - i.e. the Vendor in a B2B SaaS application scenario - to come into my apartment and, with my consent, perhaps fix things without me being there.
Multi-Tenant Data Isolation
When one thinks of isolation in a B2B SaaS application, data isolation is perhaps the first thing that springs to mind: how does one keep the data associated with each subscriber separated and secure?
In this context, data represents all that information that is particular to the way a solution works; I'm going to separate data isolation from identity isolation, and I'll cover the latter in more detail a little later 😎 In terms of data isolation, we can consider that the data for an application would typically be stored in a database - and where doing so has the added benefit of supporting all of the B2B SaaS scaling options as discussed in the Multi-Tenant Performance section below.
Isolated Tables
In a table-level isolation scenario, separate database tables are used for each individual subscriber. Table isolation often involves the duplication of more than one database table per database schema (there typically being different tables used in a B2B SaaS application supporting the various aspects of functionality). When it comes to application logic, accessing different tables within the same schema is arguably easier than switching between multiple database schemas, particularly when referencing any common tables that exist as a single instance across all subscription tenants (such as with the
JOIN
illustrated in the screenshot below).The above screenshot shows how table-level isolation is employed in WordPress Multisite, which is the technology scaffolding/framework I'm using in TheatricalPA. Each of the tables outlined in blue represents unique instances per tenant - in this case, the unique instances for the application tenant with identifier
5
- whereas the ones that aren't (outlined) represent the tables that are common across all instances.Isolated Schemas
Schema isolation often provides a higher level of confidence from a security perspective, so is typically employed in applications that need to meet more stringent requirements. However, when it comes to application logic, access across/between different database schemas - as in such cases where access to tables common across all subscription tenants is required - is typically harder. Using multiple database connections can be one solution, though comes at the expense of additional resource usage. So while there can be greater resilience to security exposure - due to errors in coding, say - it can make the code inherently more complex due to the way the logic needs to be written.
Multi-Tenant Brand Isolation
Branding can be thought of as being synonymous with the decor of an apartment in our apartment block analogy, and for B2B SaaS subscribers, branding - and by virtue, brand awareness - is important. Branding, or White-Labelling, as it's often referred to, not only gives subscription users the confidence that they are using an authorized application, but in B2B2C and B2B2B scenarios, it gives the end customer/consumer a similar level of confidence, too.
Domain White-Labelling
Domain White-Labelling - typically prevalent in a B2B SaaS Sub-domain Organization context - occurs where a totally unique domain name is used instead of the typical sub-domain organization. For example, in Auth0, one can create a Custom Domain even to the point of bringing one's own domain certificate; for instance, I use the custom domain
auth.cevolution.co.uk
for the Auth0 Tenant I've created and that I integrate into most of the applications I build (B2B SaaS or otherwise). Multi-Tenant Identity Isolation
When it comes to identity, isolation can typically be categorized in two distinct ways: (i) from an overall user population perspective and (ii) from the perspective of individual subscription Membership (discussed below). Identity isolation is where using something like Auth0 can really pay dividends: Auth0 can provide for both scenarios within the context of a single Auth0 Tenant, and where all of the platform Identity and Access Management (IAM) capabilities are overarchingly managed through the Auth0 Organizations feature! Additionally, Auth0 Connections - particularly in the case of Auth0 Database Connections - can be independently assigned to each individual Auth0 Organization to provide discrete isolation when it comes to user credentials and/or the source of user identities.
Membership
Membership is an important aspect of any B2B SaaS application. Not everyone who could potentially be a SaaS subscription user should be a SaaS subscription user! For example, corporate policy may state that not everyone in the subscribed organization should be able to access the B2B SaaS system that allows corporate travel bookings. Of course, users will need to authenticate to establish a context for authorized use, but once successful, if they're on the membership list, then they’ll typically be allowed in. More details about the membership capability in Auth0 Organizations can be found by starting with the Auth0 Docs here.
Moreover, a user could be associated with more than one subscription - something that's particularly true for contractors or third-party suppliers, say, or in the theatre industry, which is the focus for TheatricalPA. Subsequently, a user will likely want to carry their identity with them from one subscription to another - meaning no one particular subscriber should lay claim to the sum of their identity information.
For instance, in my B2B SaaS application, TheatricalPA - a subscription-based service for managing theatrical productions - I wouldn’t want the subscription-level Administrator for Dev Rel Studio Productions to be able to modify core aspects or even delete my identity within TheatricalPA...when I also use that information as part of my collaboration with Cevolution Studios. Membership is something I'll be covering in more detail in a future article, so keep an eye out for that 😁
Overall User Population
Having access to the user population overall - as in all the users that exist across all the subscriptions (tenants) provisioned in a B2B SaaS application - provides numerous opportunities for user convenience as well as additional business value. For example, whilst the Organizations feature in Auth0 provides for per-subscription context from an application tenancy perspective, the Auth0 Tenant centralized user data store provides for consistent user profiling no matter the users' membership(s).
Multi-Tenant Performance
When it comes to a B2B SaaS application, there is typically only one set of application logic employed across all tenants. At least at the core level - plugin architectures, such as those supported by WordPress, or add-on extension architectures like that supported by Auth0, also offer the ability for nominal logic to be executed on a per-subscriber basis. Irrespective, this means that from a computing standpoint, infrastructure often needs to be able to support not only the scale of users you would typically find in a B2C application but also do so at order(s) of magnitude (scale effectively being multiplied by the number of subscribers).
Rate Limiting
Protecting infrastructure becomes important, so the likes of rate limiting - where the rate at which subscription users can hit your servers is controlled - is a crucial aspect of B2B SaaS application design (even more so where APIs are also provided that allow third parties controlled access to resources). For an example of this, we can refer to the Auth0 Rate Limiting Policy, which describes how Auth0 leverages rate limiting to protect individual tenant infrastructure as well as platform infrastructure overall. Interestingly, Auth0 rate limiting can be used to help protect infrastructure in certain IAM-centric B2B SaaS application scenarios, and I'll talk more about that later.
Whilst rate limiting can offer a certain degree of control, ultimately, a B2B SaaS application will need to scale - either manually or, in many cases, dynamically - as subscribers increase and subscription usage grows. An increase in the demand for their application is, after all, what every B2B SaaS vendor aims to achieve!
Vertical Scaling
Vertical scaling is arguably the easiest to implement: in this case, scaling to satisfy demand is achieved by increasing compute, networking and/or storage (i.e. memory as well as disk) capacity. With the likes of elastic compute environments provided by third-party vendors such as AWS (see here for more details), vertical scaling can be achieved dynamically, with auto-provisioning occurring automatically based on defined policy or via the use of vendor-specific APIs. Vertical scaling is also the easiest to employ when it comes to tightly integrated database scenarios.
Horizontal Scaling
Vertical scaling, however, has a limit, and it doesn't work particularly well for scenarios requiring load-balancing - either due to geographic location or due to heavy demand from a particular subscriber. For situations where vertical scaling falls short, the option of horizontal scaling is typically where one would go next: horizontal scaling involves the incorporation of additional compute, networking and/or storage (again, memory as well as disk) resources to satisfy demand. I use the term additional here because, in horizontal scaling, one typically talks about using additional physical/virtual machines to which traffic is routed.
Once again, with the likes of elastic compute environments provided by third-party vendors such as AWS, horizontal scaling can be achieved dynamically. Horizontal scaling can also be used in conjunction with vertical scaling and typically works best when database decoupling is employed - i.e. where a separate database service is configured, such as in the case of WordPress Multisite or the like. Depending on the database service used, horizontal scaling often works better with Data Table Isolation scenarios, too.
Scaling Database Architecture
Whilst we're on the subject of databases, and data decoupling in particular, most modern database engines (particularly SQL database engines) can scale when performance becomes an issue. Database scaling is arguably not for the faint of heart. However, where the need arises, there are again options to choose from that are typically mutually inclusive.
Many database engines provide the capability to shard - which is where additional database servers/services can be employed, across which the data in a database schema (typically) is distributed. Sharding, as it's known, is often referred to as horizontal partitioning, and there's a really good article on it here that goes into the concept in much more detail than the time or the space we currently have here will allow. Sharding acts like Horizontal Scaling but at a level specific to the database. Database sharding is largely database engine specific and is beyond the scope of this article - however, there are multiple resources available on the internet that discuss how it can be achieved for the likes of MySQL, MariaDB, MS-SQL Server, etc.
Sharding isn't the most straightforward solution when it comes to database scaling, and database partitioning (abbreviated from the term vertical partitioning) may yield better results depending on what you're trying to achieve. Analogous to Vertical Scaling, this involves the distribution of data columns across multiple tables in a practice typically referred to as data normalization. Again, partitioning is largely database engine specific and is beyond the scope of this article, but there are multiple resources available on the internet that discuss how it can be achieved.
Multi-Tenant Registration
Let's now look at registration - or, more specifically, Subscriber Registration - which is typically the catalyst to tenant provisioning in a B2B SaaS application. Now the subscriber registration model that will be used is largely down to the type of solution being built. For example, you may decide to employ something like a free-tier subscription to start with - similar to what Auth0 does and what I’ve decided to offer in TheatricalPA (the B2B SaaS application that I'm building) - or you may choose to only ever register “paying” subscribers. Whichever route you take, you’ll find plenty of resources available on the web that describe the merits of each.
In TheatricalPA - my B2B SaaS Application for Theatre Production Management - registration can take one of two forms: individual subscriber registration (a la B2C style) and registration as a subscriber on behalf of a Production Company...as in the typical B2B model. In this article, I'm going to be concentrating on the B2B registration aspects and will leave the B2C aspects of registration for another time.
The image above shows the dialogue accessed via the "Register Production Company" option in TheatricalPA and is where details about the registering subscriber can be entered. I’ve built TheatricalPA to leverage Invitation workflow within the Auth0 Organizations feature to provide a fluid user experience that will also mitigate against common attack scenarios. Let's see the code that gets executed as part of the registration process:
// try { $API = new company\API(); // 1: Create Organization $response = $API->auth0()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html management()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html organizations()->create( $domain, $title); $decoded = HttpResponse::decodeContent($response); // Get the returned Organization ID if (isset( $decoded['id'] )) { $organization = $decoded['id']; // 2: New to avoid cache inconsistency $options = new \WP_Auth0_Options; // 3: Default to 'Username-Password-Authentication' Connection for the created Organization $response = $API->auth0()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html management()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html organizations()->addEnabledConnection( $organization, $connectionId = "con_omxh4cmdaRu3tQFk", $body = [ 'assign_membership_on_login' => false ]); $user = wp_get_current_user(); // 4: Send an invite to the Administrator $response = $API->auth0()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html management()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html organizations()->createInvitation( $organization, $options->get('client_id'), [ 'name' => ($user->ID) ? $user->display_name ?? $user->user_email : "TheatricalPA" ], [ 'email' => $email ], $body = [ 'roles' => [ Role::$roles['administrator']['identifier'] ] ]); } else { $error->add( $decoded['statusCode'] ?? 0, $decoded['message'] ?? ''); } } catch( Auth0Exception $ex ) { $error->add( $ex->getCode(), $ex->getMessage() ); } catch( \Exception $ex ) { $error->add( $ex->getCode(), $ex->getMessage() ); } catch( \Throwable $th ) { $error->add( $th->getCode(), $th->getMessage() ); } finally { } return( $error );
As I'm using WordPress Multisite for the technology stack framework/scaffolding, the language I'm coding in is PHP. As a consequence, I'm using the Auth0 SDK for PHP (which you can find more about here), and I'm also leveraging the Auth0 Plugin for WordPress, which I'll talk more about later.
TheatricalPA also exposes an API, so I've created the bespoke
API
class, which: (a) wraps a number of the initialization operations associated with the Auth0 PHP SDK, (b) provides a subclassable context for declaring the numerous API functions/routes within the application, and (c) enables me to easily patch the various WordPress REST API extensions I create (like the company\API()
in the code above) to consume an OAuth 2.0 Access Token as well as utilize the standard authenticated WordPress session context. As with most things "WordPress", TheatricalPA is deployed as a Plugin, and the code is typically executed as part of a WordPress Filter or a WordPress Action; all of which shall remain topics for another time 😎 In this article, I want to focus on the following things (where the notes below correspond to the numbered comments in the code snippet above):
- Here, an attempt is made to create a new Organization in Auth0. From an IAM perspective, this will represent the application tenant that will be associated with the registering subscriber.
- If successful, an attempt is then made to get configuration information from the Auth0 WordPress Plugin. At this point, execution will be in the context of the WordPress Primary Site, which is essentially the core part of my B2B SaaS application that deals with functionality outside of a specific subscription context.
- Enables the default Auth0 UserID & Password Connection on the newly created Organization, providing UserID/Password Signup and Login as standard.
- Sends the Subscriber Administrator an Invitation
Subscriber Invitation
As previously discussed, in TheatricalPA, I use the invitation workflow provided by the Auth0 Organizations feature out of the box; the screenshot below shows an example of an invitation sent to a subscriber admin. An invitation doesn’t stay around forever in Auth0, so if there’s no response within a given period, then the invite will simply time out - which is a great deterrent against fake registrations! I actually use invitations at multiple points within TheatricalPA, and I'm going to be discussing more on invitation workflow in a separate blog post at some point in the future.
I'm using capability in Auth0 to invite what is essentially the Subscriber Admin into TheatricalPA; Subscriber Administration is the topic of another blog post in this series, so stay tuned for more to come on that! 😎 To do this, I'm using the Auth0 Management API via the Auth0 SDK for PHP. The Auth0 Management API is rate-limited, so as an application developer, I can leverage this to help protect my own infrastructure. For example, if someone with malicious intent is trying to perform large-scale fake registrations, then the number and frequency of these will be limited automatically by Auth0. Auth0 can also provide me with data which could be used to potentially identify such attacks, too. Of course, I can’t rely on Auth0 API rate limiting for everything, and in other areas of the application, my own throttling will need to be implemented.
Building TheatricalPA to leverage invitation workflow as part of subscriber registration provides a fluid user experience that also mitigates against common attack scenarios. Additionally, everything in this workflow from an IAM UX perspective leverages Auth0 Universal Login - the de facto login/signup experience provided by Auth0 out-of-the-box and the interface we typically recommend using as a best practice with any integration. Universal Login integrates seamlessly with the Organizations feature, and it integrates seamlessly with other features in Auth0, too - like Auth0 Actions, for example.
Subscriber Authentication
As part of the subscriber Authentication initiated by the invitation workflow, a couple of Auth0 Actions are also implemented to help provide TheatricalPA with the information it needs. The first Action (below) is deployed as part of the
trigger and adds additional organizational attribute information to the Auth0 generated ID Tokenpost-login
exports.onExecutePostLogin = async (event, api) => { var DEBUG = event.secrets['DEBUG'] ? console.log : function () {}; var LOG_TAG = '[ADD_ORGANIZATION_ATTRIBUTES]:'; // State Machine DEBUG(LOG_TAG, "context =", event.client.client_id); switch(event.client.client_id) { // Applicable Clients? case 'aJh1tCn1UtO8i5ZgIRf2aqicoSMCmVtO': // TheatricalPA Administration case 'Nbe3RSU6myLRycWpPMF8RamQBKRaGtUi': // TheatricalPA Console case 'u4X3xedMuasp1j8kXBkD0i1kOQjagll8': { // TheatricalPA LOG_TAG += `[${event.client.client_id}]:[${event.connection.name}|${event.connection.strategy}]:`; // Add Organization name to ID Token? DEBUG(LOG_TAG, "Evaluating for Organization"); if (event.organization?.id) { api.idToken.setCustomClaim('organization',event.organization?.name); DEBUG(LOG_TAG, "Org name set: ", event.organization?.name); } } break; default: { LOG_TAG += `[${event.client.client_id}]:`; DEBUG(LOG_TAG, 'Skipping'); } break; } };
The above implementation uses logic to determine the conditions under which it will execute and also demonstrates the use of conditional debugging using the Auth0 Real-time Webtask Logs extension. For more information about the
event
object in a post-login
triggered action, see the Auth0 documentation here.The second Action is also deployed as part of the
post-login
trigger, uses conditional logic mechanisms similar to the first, and adds information to the ID Token concerning the users' assigned role(s). Out-of-box Role Based Access Control (RBAC) functionality provided in Auth0 can be utilized as part of Auth0 Organization Membership - as described here - however, if you need to employ something more flexible than RBAC then check out the following article for a discussion concerning other mechanisms that can be leveraged:exports.onExecutePostLogin = async (event, api) => { var DEBUG = event.secrets['DEBUG'] ? console.log : function () {}; var LOG_TAG = '[ADD_ACCOUNT_ROLES]:'; // State Machine DEBUG(LOG_TAG, "context =", event.client.client_id); switch(event.client.client_id) { // Applicable Clients? case 'aJh1tCn1UtO8i5ZgIRf2aqicoSMCmVtO': // TheatricalPA Administration case 'Nbe3RSU6myLRycWpPMF8RamQBKRaGtUi': // TheatricalPA Console case 'u4X3xedMuasp1j8kXBkD0i1kOQjagll8': { // TheatricalPA LOG_TAG += `[${event.client.client_id}]:[${event.connection.name}|${event.connection.strategy}]:`; // Add Roles to tokens? DEBUG(LOG_TAG, "Evaluating for Roles"); if (event.authorization) { DEBUG(LOG_TAG, "Roles = ", event.authorization.roles); api.idToken.setCustomClaim(`roles`, event.authorization.roles); api.accessToken.setCustomClaim(`roles`, event.authorization.roles); } } break; default: { LOG_TAG += `[${event.client.client_id}]:`; DEBUG(LOG_TAG, 'Skipping'); } break; } };
Multi-Tenant Provisioning
As illustrated in the screenshot below, the Auth0 Organization (in this case, Awesome Production Studios, outlined in blue) was created as part of subscriber registration to leverage the IAM capabilities of the Auth0 platform - namely the invite workflow capability together with Actions extensibility. This is an Auth0 Organization created in the context of the single Auth0 tenant I'm using for TheatricalPA.
Creating the B2B SaaS Application tenant instance is a different matter and an exercise that's largely down to the technology stack you employ and the hosting you use. In TheatricalPA, I'm leveraging WordPress Multisite hosted on AWS via the templates provided by Bitnami (see here for more details). WordPress Multisite provides a large portion of tenant provisioning out of the box - leastways when it comes to provisioning for Data and Brand isolation at any rate - and the PHP code fragment below shows how I currently leverage the
function provided as part of the WordPress API. wp_insert_site
try { $API = new company\API(); /* */ $response = $API->auth0()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-ManagementInterface.html management()-> // https://auth0.github.io/auth0-PHP/classes/Auth0-SDK-Contract-API-Management-OrganizationsInterface.html organizations()->get($id = $userinfo->org_id); $organization = HttpResponse::decodeContent($response); $company = new Type::$library['Company']($organization); /* Just-In-Time create the site. TODO: To make sure it's valid to do this, we should check the user's role in their user information, and we should also ensure the user has been through MFA (i.e. check the 'amr' value; also in the user information). */ $site = wp_insert_site([ "user_id" => $user_id, "domain" => $domain . '/', "title" => $organization['display_name'], "options" => [ // Stash the associated Company "theatricalpa_company" => $company, // Force to https 'siteurl' => untrailingslashit( 'https://' . $domain . '/' ), 'home' => untrailingslashit( 'https://' . $domain . '/' ) ] ]); if ( is_wp_error($site) ) { // Redirect to Whoops! } else { // Redirect to the newly created site wp_redirect('https://' . $domain); exit; } } catch( \Throwable $th ) { // Redirect to Whoops! }
There are a few things to note here:
The above code is being triggered via a hook registered to the
WordPress action that's defined as part of the Auth0 Plugin for WordPress. As described in the commented section of the code above, this means that from a TheatricalPA perspective, the subscription tenant is created "just in time" and only once Auth0 has validated the user. This is a great way to mitigate excessive resource usage in a B2B SaaS application due to fake registrations!auth0_user_login
I'm not showing any provisioning of resources outside of a WordPress context. For example, as discussed previously in the Sub-domain Organization section above, a subdomain would likely need to be provisioned and/or additional capacity may need to be allocated to satisfy the likes of Vertical, Horizontal and/or Database scaling (either manually or via some automatic mechanism). The specifics of this will largely be down to your application needs.
The time it takes to complete the tenant creation process may require employing an asynchronous mode of operation - i.e. where a subscriber admin is prevented from fully accessing the new application tenant until after all provisioning is complete. In this situation, Auth0 Passwordless Authentication workflows - such as Passwordless with Magic Link, say - could be employed to notify the (subscriber) admin as to when they can fully start to use their subscription.
What's Next?
In this article, I've walked through the details of Multi-Tenancy in a B2B SaaS Application and also provided some insight together with code examples for how I'm integrating with Auth0 to help build multi-tenancy in my application, TheatricalPA. I'll be writing more about the other topics I've mentioned in this article in the near future, so be sure to watch out for those. In the meantime, feel free to comment below and tell us what you think - we always love to hear feedback, positive or otherwise, as it helps us to improve our content! Thank you. Aside from that, here are some additional resources you might like to follow up on that can help you on your journey. Have fun, and I'll catch up with you next time! 🤗
About the author
Peter Fernandez
Principal Developer Advocate