Valery Melou
Articles
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

foreign key on each table scopes the data. This is a common and highly scalable pattern.

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

. Then, we need an easy way to mark other models as "belonging" to a company.

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:

python

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.

python

This simple setup gives us a flexible way to manage the user's context. They have a default company, but we can change their

for the duration of a request if they decide to switch tenants.

3) Inject Tenant Context During Authentication

This is where the real magic happens. How does

get set on every request? We create a custom JWT Authentication class that intercepts the request after the user is authenticated.

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.

python

Then, we just tell DRF to use our custom class in

. From now on, every authenticated request will have a
object that knows which company it's supposed to be operating 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.

python

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.

python

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

. For example, if I were to use that in our timesheets application, this is how I would define the tenant scoped models
,
, and
.

python

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.

python

When a user makes a

request to
, the
automatically filters the queryset to
. When they
to create a new customer, it automatically sets the company for them. Secure, automatic, and invisible.

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:

  1. A user sends a request with a valid JWT.

  2. Our custom

    backend validates the token and sets
    .

  3. The request is routed to a

    like
    .

  4. Because it inherits from

    , its
    method is automatically called, which filters all customers to only those belonging to the user's active company.

  5. 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.