- Date
Building a Multi-Tenant SaaS API with Django Rest Framework: The Shared Database Approach

Building a secure, multi-tenant SaaS application presents unique challenges. This article chronicles a practical experiment in building a tenant-aware API using Django Rest Framework. I will go through the entire process, from defining tenant-owned models to creating custom authentication backends that set a per-request tenant context, and finally, leveraging mixins to automate data isolation in the views. This guide is my approach for building multi-tenant systems in Django using a shared database and shared schema.
A few weeks ago, I kicked off an interesting experiment. Our team at MBOA DIGITAL is very comfortable with NestJS, but I wanted to see how another framework would handle a core challenge for any SaaS application: multi-tenancy. The goal was to evaluate how quickly I could implement a robust, tenant-aware API using Django and Django Rest Framework (DRF).
The challenge: Can a user log in once and securely access data from only their company, completely isolated from other companies on the platform?
I decided on the "Shared Database, Shared Schema" approach, where all tenants' data lives in the same database, but a
1) Define the Tenant and Tenant-Owned Models
First things first, we need to define what a "tenant" is. In our case, it's a
I created three key models:
: The tenant itself. : An abstract base model that any other model can inherit from to automatically get a foreign key. This is the magic that scopes our data. : A "through" model that links users to companies and assigns them a role (like Admin or Staff).
Here's the code:
With this foundation, any model that needs to be isolated per tenant can simply inherit from
2) Make the User Model Tenant-Aware
A user can be a member of multiple companies, so we need a way to know which company they're currently working in during a single request.
I extended the standard Django User model with two important properties:
: A persistent field in the database. This is the tenant the user will see when they first log in. : A temporary, per-request property. This is set by our authentication layer and is the key to filtering data for the current session.
This simple setup gives us a flexible way to manage the user's context. They have a default company, but we can change their
3) Inject Tenant Context During Authentication
This is where the real magic happens. How does
It authenticates the JWT as normal, and then, as an extra step, it looks up the user's default company and attaches it to the user object.
Then, we just tell DRF to use our custom class in
4) Create Reusable ViewSet Mixins
To avoid repeating the same filtering logic in every single view, we created a couple of mixins. These are small, reusable classes that provide tenant-scoping utilities.
This is the power of Django's class-based views. We've encapsulated all the core multi-tenancy logic here. Our actual views will be incredibly clean.
5) Assemble Base ViewSets
Now, we just assemble these mixins into convenient base classes that our actual ViewSets will inherit from.
From now on, for any resource that belongs to a company, we'll just use TenantModelViewSet.
6) Make Domain Models Tenant-Owned
With the infrastructure in place, applying it to our business models is trivial. We just need to make sure our models that are tenant scoped inherit from the
The key here is the inheritance and updating any UniqueConstraints to include the company field.
7) Implement the Final ViewSets
This is the payoff. Look at how clean our ViewSets are. All the complex filtering and tenant assignment logic is completely hidden away in our base classes. We just define the queryset and serializer, and it just works.
When a user makes a
If you want more confidence like me, you can (and you definitely should) add unit tests for your endpoints to validate that the data is really scoped to each tenant.
8) The Final Flow
Let's recap the entire process for a single API request:
A user sends a request with a valid JWT.
Our custom
backend validates the token and sets . The request is routed to a
like . Because it inherits from
, its method is automatically called, which filters all customers to only those belonging to the user's active company. The data is serialized and returned, with the user being completely unaware of any other companies' data in the database.
Conclusion
So, how did this experiment compare to our usual workflow with NestJS? It was surprisingly straightforward. Django's "batteries-included" philosophy really shines here. The powerful ORM, class-based views, and mature ecosystem for things like DRF and Simple JWT meant I didn't have to build much from scratch.
While NestJS gives you incredible flexibility, the Django approach felt more structured and guided for me. I was able to build a secure, production-ready multi-tenant system with a few well-placed classes and a clear, logical flow.
Now, I would like to hear how you handle multi-tenancy in your SaaS when using a shared database with shared schema. Feel free to ping me on X @valerymelou to start the discussion.