DEV Community

Prince
Prince

Posted on • Originally published at kostra.io

Next.js Multi-Tenant Architecture Patterns for SaaS

Next.js Multi-Tenant Architecture Patterns for SaaS

Developing a successful B2B SaaS application is not limited to delivering great features; it is also about building an architecture that can support multiple tenants efficiently without compromising data security and customizability. The concept of multi-tenancy is now the foundation of the SaaS economy. With this model, companies can support thousands of customers through a single instance of their application. In this guide, we will explore different architecture patterns that can help developers build multi-tenant applications on Next.js and TypeScript.

What is Multi-Tenancy in the Context of SaaS Applications?

In the context of SaaS applications, multi-tenancy is defined as deploying multiple tenants in a single instance of the application. Each of the tenants in this system is made to believe that they have their own dedicated application instance. The application instance is customized to their needs and is given its own brand and configurations. The concept of multi-tenancy is what makes B2B SaaS applications economically viable.

When it comes to choosing the architecture of the multi-tenant system, it can affect everything from development speed to cost. Choosing the right architecture can help you add new customers to your B2B SaaS application in minutes. On the other hand, choosing the wrong architecture can lead to months of refactoring code to add new features that your architecture is unable to support. The problem is even more complex in B2B SaaS applications, where customers have high compliance requirements.

Multi-tenant SaaS architecture diagram showing tenant resolution, RBAC, data isolation, billing, caching, and scaling patterns

Why Next.js and TypeScript are Better than Other Technologies for Developing Multi-Tenant Applications

When considering frameworks for multi-tenant architecture, there are some compelling reasons to choose Next.js, and these will become apparent as the complexity of the project increases. The hybrid approach to rendering with Next.js will allow you to provide tenant-specific content with excellent performance. A React Next.js boilerplate will provide you with API routes, database connections, and middleware.

When you're working with tenant contexts, having access to TypeScript will go from being a 'nice to have' to a 'must have.' The type safety provided by TypeScript will prevent you from having entire classes of bugs. What if you were to accidentally query data from the wrong tenant? With TypeScript, you can ensure at compile time that tenant ID information is correctly propagated throughout the application. A Next.js starter template with TypeScript will provide you with the guardrails that will prove invaluable as the project grows and more people contribute.

The Three Primary Multi-Tenant Architecture Patterns

The architecture you choose will have a fundamental impact on the scalability, cost, and operational overhead of your application. There are three main patterns, and each of these patterns will have different implications based on the business model and the stage of the business.

The shared database, shared schema pattern is the highest level of resource sharing. In this model, all tenants will share the same database and the same schema. The only difference will be a tenant identifier column. This model will provide the highest efficiency and lowest operational overhead. This is the best choice for startups and small to medium B2B SaaS businesses. Your Next.js boilerplate will include tenant resolution middleware, which will add the tenant ID to every database query.

The shared database, separate schema architecture, while more complex in terms of isolation, also offers more simplicity in operations. In this architecture, each tenant is provided their own schema within a shared database instance. This architecture is good for situations where you need to offer customization per tenant, such as additional fields or even new tables, which is what many b2b saas enterprise customers require. The problem is managing schema upgrades across thousands of tenants, but it is for good reason.

The separate database per tenant pattern offers the maximum isolation and customization opportunities. Every tenant is given a separate database, and this offers maximum isolation and customization opportunities. The separate database per tenant pattern is the most suitable pattern in meeting the needs of enterprises, but is the most complex pattern in terms of operational needs. Your Next.js bootstrap boilerplate needs the most complex connection pooling and tenant routing to make the pattern operational.

Implementing Tenant Resolution and Context

Every time a user makes a request in your multi-tenant application, you need to answer a fundamental question: What tenant is this request associated with? The best and most common methods of resolving the tenant are subdomain-based routing, path-based routing, and header-based tenant resolution. The most common and best pattern in b2b saas is subdomain-based routing, and this is the most common and best pattern in meeting the needs of enterprises.

In a Next.js boilerplate, the tenant resolution is done in the Next.js middleware. The middleware extracts the tenant identifier from the request and then validates the tenant identifier before injecting the tenant context into the request object. The tenant context is then passed through the application and into the API routes, database queries, and server-side rendering functions. The most important aspect is making the tenant context immutable and thread-safe so that you do not end up leaking the data of one tenant to the next.

Database Design Patterns for Multi-Tenant Data Isolation

Your data layer is the most critical component in making your multi-tenant architecture a success or a nightmare. The shared schema pattern is the most common in multi-tenancy, and in this pattern, you need a tenant ID column in every table in your database and need to include the tenant ID in the WHERE clause of your queries. The problem is that you are likely to have hundreds of queries in your application, and a small mistake in the queries could end up leaking the data of Customer A to Customer B in your Next.js saas starter using TypeScript.

An additional layer of protection is row-level security implemented on the database. PostgreSQL and other modern databases support policies that can automatically filter rows based on session variables. You can specify the context of the tenant when opening your database connection. The database will enforce row-level security, even when your application queries are not specifying this. This is a defense-in-depth approach to row-level security, greatly reducing the chance of data leakage bugs making it to production.

Indexing in multi-tenant databases is a special case. You'll want to use compound indexes on your tables, ensuring that your queries will perform quickly even as individual tenants' data grows. However, keep in mind that database metrics are just as important to monitor, since explosive growth in one tenant can affect all tenants in a database setup.

Authentication and Authorization in Multi-Tenant Systems

Authentication is a more complex issue in multi-tenant applications. Users can be a part of many different tenants. Your Next.js Auth0 integration or other similar integration will need to allow a user to select which tenant they are a part of when they log in. In addition, users may have different roles in different tenants. For example, a user may be a part of Organization A and Organization B:

  • In Organization A, they are an admin.
  • In Organization B, they are a read-only user.

There are two main steps in user authentication in multi-tenant applications. First, you need to verify your user. Then, you need to verify what context they are accessing. Your Next.js starter template will include a number of different pieces of middleware to verify that a user is authenticated and authorized to access a certain tenant. It will not allow a user to access a different tenant just because they are authenticated.

Role-based access control must be tenant-aware throughout the application. When determining if a user can perform a given action, we're really asking: "Can this user perform this action within this tenant?" Our types should be designed to make it impossible to check permissions without the tenant context. This helps catch permission-related bugs before they hit production.

Handling Tenant Specific Customization

B2b saas enterprise customers expect customization beyond branding. This can include custom fields, customizing the workflow, and integration settings. Your application architecture must support this without introducing technical debt that hinders future development.

Using feature flags for the tenant can help provide a solid way to handle customization. This will allow a feature to be enabled for a tenant but disabled for another. This is very useful for testing a feature with a friendly tenant before rolling it out to the entire platform. Your react Next.js boilerplate should include a feature flag system that integrates with the tenant context very well.

Custom domains can greatly enhance the perceived value for enterprise customers. This allows the customer to access the application at a subdomain of their company's domain. For example, instead of accessing the application at theircompany.yourapplication.com, they can access it at app.theircompany.com. This greatly enhances the white-label application. This requires the ability to manage the domains and the SSL certificates for the application. Most reactjs boilerplate examples will include a way to handle this, but it requires a great deal of attention to security and certificate management.

Stripe Integration for Multi-Tenant Billing

Billing with a multi-tenant system presents some interesting challenges, especially if you're using Stripe. You'll need to ensure each tenant is independently billed, subscribed, and has their payment methods. Your stripe developers integration should create separate customers in Stripe for each tenant, not users under each customer unless you're in a business model where users need billing.

The choice you're making with this architecture will have a ripple effect on your entire billing system. In most cases, with a B2B SaaS, it's the organization itself that pays, not the end users. So, your stripe integration will need to interact with the tenant records, and changes to subscriptions will need to affect all users under this tenant. Your next.js bootstrap boilerplate will need patterns for handling subscription upgrades for all users under the tenant at once.

Usage billing will prove to be especially complex in a multi-tenant system. You'll need to ensure you're aggregating usage across all users under a tenant, reporting this to Stripe, and then billing the correct tenant. The type system will help you ensure the usage events have the correct tenant context so you don't end up with the nightmare scenario of one tenant being billed for another.

Performance Optimization for Multi-Tenant Applications

Multi-tenant systems present unique performance challenges. A bad tenant can negatively impact all tenants with a shared infrastructure approach. Your Next.js saas starter will need patterns for tenant isolation at the performance level, not just the data level.

Database Connection Pooling

In a separate database per tenant system, you can't keep a database connection open for all tenants. You may have thousands of tenants. Your database connection pool will need to be dynamic, opening a new database connection on the fly and closing it aggressively. This will add latency to the first request for each tenant.

The caching strategy should be tenant-aware to avoid cache poisoning attacks and data leakage. Cache keys should always be constructed with tenant IDs, and cache invalidation should be able to clear cache for each tenant. A shared cache where one tenant's cache can be served to another is a serious security threat, and TypeScript cannot protect against this threat alone.

Scaling Strategies for Growing Multi-Tenant Platforms

As your B2B SaaS platform grows, scaling becomes a multidimensional problem. You are no longer just scaling out to support more capacity, you are scaling across different types of tenants with different usage patterns. Some tenants might have ten users with occasional traffic, while others have thousands of users with constant traffic.

Tenant sharding can be used to scale infinitely in shared database architectures. Instead of all tenants being in one database, you can distribute tenants across several different databases based on some sharding criteria. You can then assign new tenants to the least loaded shard, and large tenants can be moved to separate databases. Your react Next.js boilerplate should be able to support different sharding patterns for each tenant.

Horizontal scaling for your Next.js servers is easy, but routing for each tenant can be tricky. If you are using in-memory cache or session stores, you'll want to implement session affinity to ensure all requests from a given tenant go to the same server. You could also move all shared state to Redis, which has higher latencies.

Security Considerations and Compliance Requirements

Security in multi-tenant systems requires paranoia. You are no longer protecting data from outside attackers; you are protecting each tenant's data from every other tenant. A security flaw that allows access to other tenants' data can ruin customer trust and cause enormous compliance issues.

Regular security audit checks must also cover security against other tenants:

  • Can authenticated users access other tenants' data?
  • Can they enumerate other tenants within the system?
  • Can they use timing attacks to gain information about other tenants?

Answers to these questions must exist before you sign up your first enterprise customer that requires security to be taken seriously.

Compliance requirements

Compliance requirements such as GDPR, HIPAA, and SOC 2 can get even more complicated in multi-tenant systems:

  • Can you delete data for one of your tenants without affecting other tenants?
  • Can you export a complete dataset for one of your tenants?

The code within your Next.js starter template must have examples of how to export and delete data that actually work in your chosen architecture pattern.

Monitoring and Observability Per Tenant

Production monitoring for multi-tenant systems requires monitoring each tenant individually. When errors start spiking in your system, you need to know if it's across the board or if it's isolated to one of your tenants.

Application logging must include information about which tenant each log is related to. When debugging issues, being able to filter logs per tenant makes debugging go from impossible to simple. Using TypeScript to log information ensures that the tenant ID is propagated through each log message. Your Next.js starter template must have examples of how to log this information.

Metrics and alerting need to be segmented by tenant. You need to monitor the request rate, error rate, and response time for each tenant. You need to be able to alert on a sudden increase in the error rate for a single tenant, which could mean a problem with that tenant's setup. This level of detail helps you keep your service level agreements with each customer.

Testing Multi-Tenant Applications

Testing a multi-tenant application is much more complicated than testing a single-tenant application. You need to make sure the application does the right thing, but you also need to make sure it does the right thing for a multi-tenant application:

  • Can the user see only the data for the tenant he or she belongs to?
  • Does the tenant-specific configuration work correctly?

You need to make sure that integration tests include tests for the application's ability to handle multiple tenants. This can be done by creating a number of test tenants and verifying that the application keeps them separate. You should be able to write a test where data is created for Tenant A and verify that Tenant B cannot access it. This catches the little mistakes that you would otherwise catch with a code review or a unit test.

End to end tests need to include tests for tenant switching:

  • Can a user who belongs to many tenants switch between them correctly?
  • Can the application keep the correct tenant context for a complex application?

Your Next.js saas starter should include tools for testing the application that make it easy to write tests for the application, rather than an afterthought.

Common Pitfalls and How to Avoid Them

The most treacherous pitfall of a multi-tenant application is the tenant filter omission. You write hundreds of queries, and one of them forgets the tenant filter. This query will then return data for all tenants, which is a security breach. You can avoid this pitfall by never trusting your developers to remember to add the tenant filter. This can be done with query builders or ORMs that enforce the tenant filter on the type level with TypeScript.

Another mistake is failing to plan for tenant growth imbalances. Your first 50 tenants might be small businesses, but the 51st might be a corporation with 100x the usage of your previous largest tenant. If you have not planned for this, you might have to refactor your system while trying to close your largest sale.

Failure to plan for the operational complexity of a multi-tenant system leads to scaling bottlenecks. It's easy to migrate your schema if you have a shared schema. However, if you have 5,000 separate schemas, you'll need to plan for the operational complexity of the migration process.

Migrating Between Multi-Tenant Patterns

In other cases, your initial decision might not have been the best, especially if your business evolves. It might have started as a shared database, then later as a shared schema. However, you might have to migrate your system from a shared schema to separate databases. Planning for the migration process from the beginning helps you avoid the pain of migrating.

The best way to plan for the migration process from the beginning is to abstract your resolution of the tenants and the data access behind interfaces. If your entire system directly accesses the databases, you might have a very difficult time migrating. However, if your Next.js starter template has a data access layer, you can easily migrate without affecting your entire system.

Planning for the migration process from the beginning allows you to migrate your system one tenant at a time. However, you might want to migrate your largest or most demanding tenants first.

Conclusion

Multi-tenancy patterns for B2B SaaS applications constitute one of the most complex challenges in software development. Everything, from the speed of development to security and even financial performance, is influenced by the choice between shared and isolated resources. Starting with a strong react Next.js boilerplate with typescript gives you a strong foundation upon which to implement these patterns correctly. However, the success of implementing these patterns correctly will depend on the knowledge of the trade-offs and the decision-making process.

The patterns presented in this article, ranging from database isolation patterns to tenant-based authentication and customization, have been tested and used by thousands of B2B SaaS companies. Whether you start with a Next.js boilerplate or start from scratch, these patterns will help you design an architecture that will scale with your business and meet the security and isolation requirements of enterprise customers. Multi-tenancy is not just a technology; it is the economic engine behind the success of SaaS.

Top comments (0)