The Enterprise Browser's Approach to Multi-Tenancy Architecture
Let’s explore how to choose a multi-tenancy approach that achieves both security and velocity.

In B2B SaaS applications, it is crucial to keep data not only secure, but also well separated between organizations (referred to as “tenants” in this context). A breach in tenant isolation could expose proprietary data, financial records, or customer information to competitors or unrelated organizations. This is not only a serious trust issue but can also lead to legal and regulatory consequences under data protection laws like GDPR.
Naturally, when we laid the foundations of the Island Enterprise Browser, one of the first architectural decisions was how to keep our customers' data well-isolated so that one organization’s data is never accessible to another. In other words, we needed to decide what multi-tenancy approach we should take.
This decision affects many parts of our company - from product security to resource costs and even development speed. That’s why it’s crucial to be well-informed, use your resources and time wisely, and create a product that best meets your customers’ needs, whether you’re starting something new or improving an existing one.
Let’s explore different approaches to multi-tenancy architecture and the pros and cons of each in helping organizations choose the best fit for their product.

The tenant-landlord dynamics in multi-tenancy architecture
A single B2B SaaS application typically serves multiple tenants (customers), requiring the separation of each tenant’s data. Multi-tenancy architecture addresses this issue, finding effective solutions in the world of cloud applications and costly cloud resources.
There are two main approaches to multi-tenancy architecture, which can be easily understood through the metaphor of a residential building, where each customer or organization is a separate tenant.
Imagine a residential building with two types of apartments: shared and single-occupancy. Shared apartments are considered more affordable because tenants share resources such as the kitchen, bathroom, and laundry machine. However, if one of your roommates takes extremely long showers, everyone pays the price. And if you’ve ever lived with roommates before, you’re probably familiar with the lack of privacy that comes with shared living.
On the other hand, single-occupancy apartments provide all the privacy you need because you are completely separated from the rest of the building. The downside is that you must equip yourself with all kinds of resources, such as a dishwasher and a laundry machine, even if you won't use them very often, making it quite costly. However, at least you are only paying for your own usage.
Now, let's assume your landlord needs to tend to something in all the apartments in the building. For single-occupancy apartments, this requires visiting each apartment individually. For the shared apartments, on the other hand, they only need to visit once to make several tenants happy, which is significantly more efficient.
The same concept applies to SaaS applications. There are two main types of multi-tenancy architectures: shared database/database-per-tenant and isolated tenant.
.png)
Shared database architecture (a.k.a roommates' apartment)
In this approach, we have a single database for a single application. The data of all tenants is stored on the same database, but separately - either with a tenant identifier (tenant ID) column or with a different schema for each tenant.

Isolated-tenant architecture (a.k.a single apartments)
With this approach, each tenant has their own dedicated cloud resources, like databases. Sometimes, there is even a different deployment for each customer, but we will focus on the single-deployment and database-per-tenant case.
.png)
Shared database vs. isolated tenant
Let’s examine a few points for comparison between the approaches.
Isolation
Tenant data isolation is a necessity for all B2B SaaS applications. However, in some cases, it’s more critical than in others. For example, if customer data is highly sensitive or includes PII, we simply cannot afford the risk of cross-tenants' data leakage.
With the shared database approach, there’s a risk of cross-tenant data leakage - meaning one tenant could accidentally gain access to another’s data. For example, this can occur if, due to a bug in the application’s code, we fail to filter results by the requesting tenant. For example, consider this database query:
SELECT * FROM TASKS WHERE VISIBLE=TRUE OR STATUS="DONE" AND TENANT_ID='Org1'In this example, the application, on behalf of a user from Org1, accidentally queries tasks from other tenants as well, if they are VISIBLE=TRUE.
However, with the isolated tenant approach, the isolation is made on the database level, so it is much harder for data to be leaked from one tenant to another, making it the safer choice here.
Cost
Cloud costs matter to all of us, from small startups to well-established enterprises. However, if your team is on a budget, you are probably looking to reduce costs wherever possible, so you choose the most resource-efficient solution.
With the shared database approach, we are only paying for one database. Since the resources are shared, the environment uses its full potential for capacity, resulting in dramatically lower cloud costs. By comparison, in the isolated tenant approach, we pay for a new database per tenant.
Maintenance
With the shared database approach, maintenance work like upgrades and database migrations is seamless. Similar to the example of the landlord fixing an issue in every apartment, it’s easier for us to fix an issue or to run a database migration only once every time.
However, with the isolated tenant approach, maintenance may become a nightmare when each migration runs on hundreds or thousands of customers. What happens if the migration fails in one of the databases? How will we fix each one separately?
Scale & bill by the tenant
Island’s customers use our cloud resources very differently - with some storing much more data or running heavier queries than others. This means we want the flexibility to scale their resources specifically or to charge them based on actual usage.
With the shared database approach, we face the ‘noisy neighbor’ problem: one tenant is consuming excessive resources (such as frequent, heavy database queries), resulting in a downgrade in application performance for everyone else.
Shared resources also make it harder to bill tenants fairly according to their usage (much like a roommate who takes extra-long showers and drives up the water bill for everyone). In these cases, the isolated tenant approach is often a better fit. If a specific tenant’s database is loaded, for example, it won’t affect the other tenant's bill or application performance.
Breaking down the walls
In some cases, we need to fetch tenant data from a different tenant; for example, when a company has subsidiaries, or when shared/static data is accessible to all customers.
With the shared database approach, we can have shared data by not having a tenant ID column in those tables. On the same principle, we can query the database for all the subsidiary companies.
// Static data
SELECT * FROM COUNTRY_CODES // No need for TENANT_ID filter
// Subsidiary companies
WHERE TENANT_ID IN (Org1, Org2, Org3)However, with the isolated tenant approach, we have to replicate the static data for each database, which risks failure and data inconsistencies. Having separate resources makes it harder to share and join data between the subsidiary companies.
Tenant onboarding
With the shared database approach, onboarding a new tenant is usually easier than in the isolated tenant approach, since no new resources need to be acquired and utilized.
Comparison summary
Here’s a summary of the different approaches and their pros and cons:

In reality, there is a whole spectrum of multi-tenancy implementations that involve aspects from both approaches. In a single-tenant architecture, for example, admins in charge of a few tenants can view data from all of their tenants together. Another example is a single database for all tenants, but a different schema for each tenant inside the database.
Best practices
No matter which method - or combination of methods - you choose, it’s important to follow these multi-tenancy best practices:
A. Know your customers (prepare for isolated tenant)
For most customers, your data architecture won’t be a major concern as long as data is well-isolated and stored securely in compliance with regional regulations like GDPR. However, some organizations face strict requirements mandating separate databases, sometimes with their own encryption keys, such as customer-managed keys or KMS (Key Management Service).
Enterprise customers increasingly expect BYOS (Bring Your Own Storage) - the ability to store and control data in their own cloud resources while still using your product’s features. BYOS is another form of single-tenancy that’s worth planning for early.
If you work with customers requiring KMS or BYOS, prepare in advance for isolated tenants. Avoid tightly coupling your code to a single connection string, encryption key, or cloud provider. Instead, make these configurable and derivable from the HTTP request context, so each customer can seamlessly use their own data solution.
B. Isolation by design (to avoid mistakes)
Since customer data is extremely sensitive, your infrastructure should accommodate seamless data separation implementation for developers, to avoid human errors (according to the risks of each method).
For example, in your application code, we can use a provider that puts the correct tenant ID in the context of the HTTP request according to the given authorization headers according to the HTTP request context:
public class TenantIdMiddleware{
public override async Task InvokeLogicAsync(HttpContext httpContext, IRequestContext requestContext) {
requestContext.TenantId = cookie.TenantId;
}
}By having the right tenant ID on the request context, we can then utilize a middleware that injects the correct database connection string according to the tenant ID, making it seamless for developers.
Some practical examples
For shared-database architecture:
The main risk of shared-database architecture is data leakage between tenants. In this case, we should add data leakage validation to every layer of our application.
For example, at the application level, we make sure new data objects in our system are ready to be consumed by different tenants, by having a base object that has a tenant ID field and from which all data objects will inherit:
public abstract class BaseTenantObject {
public string TenantId { get; set; }
}We can even add a step in our development process to keep every entity in our system “tenant-ed” (meaning it has a tenant ID), by using pre-compilation rules or a linter (depending on the framework in use). For example, in .NET we can use Roslyn Analyzers:
foreach (var property in properties) {
DiagnosticUtils.ReportError(context, "INVALID_ENTITY_PROPERTY", "TenantId", "Object must implement inherit BaseObject for a TenantId validation", obj);
}In the data access layer, we add a built-in tenant filter for every query we are performing:
protected override void DefineGlobalQueryFilters(ModelBuilder modelBuilder) {
modelBuilder.ApplyQueryFilters<TaskEntity>(a =>
a.TenantId == RequestContext.TenantId);
}In the application layer, before returning any data to the client or sending it to another service, we can validate that the tenant ID of the results matches the one we injected in the request context and throw an exception if they do not match.
foreach (var entity in result) {
if (entity.TenantId != RequestContext.TenantId) {
throw new BadRequestException()
}
}For isolated tenant architecture:
The main risk of isolated tenant architecture is data migration failure for one of the databases, causing data inconsistencies.
Therefore, it’s essential to detect errors as soon as possible (preferably in CI). For example, when running a migration on several dockers, each docker represents a different test tenant database. If migration fails on one of the test tenants, the CI will fail.
Another crucial thing is to quickly detect and fix migration errors in production as soon as they occur. We should set up proper monitoring to alert us if something goes wrong (which it will 🙂), using tools like Datadog or Prometheus + Grafana, and implement an efficient rollback mechanism for migrations. For example, wrapping migrations in transactions where supported, or keeping a reverse migration script, so you can revert changes across all tenant databases and re-run them after applying a fix.
C. Add processes to maintain multi-tenancy security every step of the way
While it’s important to make multi-tenancy implementation seamless for developers, it’s crucial that it’s not overlooked and that everyone in the process (developers, QA, PM, and managers) always keep multi-tenancy in mind when designing, developing, and testing new features.
At Island, we achieve this by running regular automated checks, such as periodic tests to verify our multi-tenancy restrictions. For example, there can be an integration test that tries to fetch objects of tenant A while being authenticated as tenant B, and expects this fetch to fail.
Another way to achieve this is by running regular penetration testing focused on multi-tenancy security.
Education plays a crucial role, too. This includes conducting secure coding workshops with an emphasis on tenant isolation, incorporating multi-tenancy training into developer onboarding, and ensuring thorough design and code reviews with tech leads or software architects.
To summarize
Now that we’ve discussed multi-tenancy architectures and the best practices for each approach, it’s important to remember that each choice is a good choice as long as it meets your product and business requirements. Being aware of its pitfalls and handling them well while also following best practices is crucial.
Which method did we choose at Island? (Hint: maybe you can have the best of both worlds?) If you're curious to find out, come and join us.

.png)

