Simplify SaaS App Form Customization With Django Tenant Options

Jack LinkeJack Linke
5 min read

I've been working on a Django package for a couple of years now that solves a specific, but common problem in B2B SaaS applications. If you've ever built a multi-tenant application where different business customers need to customize form options while maintaining some standardization, this might be exactly what you're looking for.

The Problem

A situation I've encountered repeatedly when building B2B applications is that tenants need to collect information through forms, but each tenant wants their own spin on the options they show the end-user. But, you may need to ensure certain choices are always available across all tenants for consistency or compliance reasons, and some tenants don’t want to create a form’s options from scratch - they want default options that make sense to start with.

Here's a concrete example: Imagine you're building a coffee ordering platform for businesses. Each business (tenant) needs a form where their customers can select their preferred coffee type. Simple enough… but then it gets tough:

Tenant A has a traditional coffee shop and wants to offer: Drip Coffee, Espresso, and Cold Brew. Tenant B has a specialty café, preferring Pour Over, Espresso, and Horchata Lattes! As the platform owner, you want to ensure "Espresso" is always an option (maybe you get a kickback from the Espresso Guild for the number of espressos ordered through your SaaS app).

Some Approaches… and their Headaches

When I first encountered this problem, I explored several common approaches. Each one left me disappointed in the compromises I’d have to make.

A CharField with choices seems like the obvious first solution. It's simple to implement, and Django handles makes it easy to add to a form. But the moment a tenant asks for a custom option, you've got to create a new migration. Not great 🙄

The next thing to try might be a ManyToManyField with a custom through-model. This adds flexibility, but now you're juggling the complexity of maintaining default options and figuring out how to enforce mandatory choices across all tenants. Complexity grows quickly 😓

Some developers reach for JSONField at this point. It’s flexible, but you lose schema validation and referential integrity. They're great for storing json data, but not an ideal alternative for most django fields. And querying is a bit less intuitive than working with traditional models/fields in the django ORM 😬

And then there's the nuclear option: custom models per tenant. I can’t imagine a situation where this would end well. The increase in migrations alone should scare you away from this approach 🤢

So, django-tenant-options!

After wrestling with these approaches in my projects, I built django-tenant-options to solve the problem once and for all. The package provides a structured way to:

  1. Define mandatory options that appear in every tenant's forms

  2. Offer optional defaults that tenants can choose to use (makes it easier for tenants who don't want to spend time starting from scratch)

  3. Allow tenants to create their own custom options

  4. Maintain data integrity even when options change

Here's a realistic example - a financial document management system where employees upload statements and must specify the document type as part of the form. Let's assume you already have a Tenant model in your Django project.

In django-tenant-options, every "Choice" model is paired with a "Selection" model. Options at the things a tenant can choose from or create. Selections are the resulting subset of Options selected by each Tenant to be shown in their resulting form.

from django_tenant_options.models import AbstractOption, AbstractSelection  
from django_tenant_options.choices import OptionType

class DocumentTypeOption(AbstractOption):
    tenant_model = "yourapp.Tenant"  # Your tenant model
    selection_model = "yourapp.DocumentTypeSelection"  # Defined below
    default_options = {
        "Balance Sheet": {"option_type": OptionType.MANDATORY},  # Always shown
        "Cash Flow Statement": {"option_type": OptionType.OPTIONAL},  # Tenant's choice
        "Fund Flow Statement": {"option_type": OptionType.OPTIONAL},  # Tenant's choice
    }

class DocumentTypeSelection(AbstractSelection):  
    tenant_model = "yourapp.Tenant"  # Your tenant model
    option_model = "yourapp.DocumentTypeOption"  # Defined above

The magic here is in the simplicity. You define your options once, specify which ones are mandatory versus optional, and let your tenants customize from there. The package handles the complexity of maintaining these options and ensuring data integrity.

Behind the Scenes

What makes this package powerful is how it handles the complex cases that usually cause headaches in multi-tenant applications:

Soft deletion ensures that even if a tenant removes a custom option or a selection that was used in existing forms, historical data remains valid and queryable. This has saved me countless hours of data cleanup and maintenance.

Database triggers (optional but recommended) maintain referential integrity between tenants and their options. You probably want this guarantee at the database level.

The package integrates with Django's forms, limiting the code you need to write.

When Should You Use This?

While django-tenant-options might seem niche, I've found it valuable in several types of applications. It's particularly useful if you're building:

  • A SaaS platform where different businesses need their own customized forms but you need to maintain some standardization, e.g.: HR systems, project management tools, or any B2B platform where form customization is important.

  • Applications where you need to balance flexibility with consistency. The mandatory/optional system lets you give tenants freedom while maintaining control where it matters.

To get some ideas on the types of forms where this might be helpful, we have an Options Cookbook. While some of the custom options are a bit silly, I think you'll get the idea 😁

Getting Started

Installation is straightforward:

pip install django-tenant-options

The package has a good amount of documentation, and I've put significant effort into making it easy to integrate into existing projects. You can find the full documentation at django-tenant-options.readthedocs.io.

~ Fin ~

Building this package has taught me a lot about the challenges of naming projects (the package was originally named flexible-list-of-values), building multi-tenant applications, and the importance of finding the right balance between flexibility and standardization without adding too much magic 🔮. While it might seem like a niche solution, I've found it solves a real problem that comes up surprisingly often in B2B applications.

I'd love to hear your thoughts and experiences with similar challenges. Have you tackled this problem differently? Are there use cases you think this package could help with? Have recommendations for improvements? Let me know in the comments or open an issue on GitHub.

0
Subscribe to my newsletter

Read articles from Jack Linke directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jack Linke
Jack Linke

Jack Linke tends to learn the hard way - and shares the lessons from those experiences with others through blogging, tweets, and speaking engagements. He has been developing software and hardware projects off-and-on for most of his life, but much of his relevant web development experience has been hard-earned over the past four years during development of Watervize - a B2B2B SaaS web application to help irrigation water utility companies improve efficiency, analysis, and communication with staff and agriculture customers. Jack's technology interests include Python, the Django project, HTMX, GIS, graph theory, data storytelling, and visualization. He is a frequent contributor to the open source community and a contributing member of the Python Software Foundation. Outside of coding, Jack serves as a Marine Corps Officer, solves unusual math and logic problems, and makes a mess in the kitchen. Open Source Projects: django-postgresql-dag - Directed Acyclic Graphs with a variety of methods for both Nodes and Edges, and multiple exports django-calendardate - A Django calendar model with date metadata for querying against django-htmx-todo-list - An example of todo list application using Django and HTMX Mastodon (@jack@jacklinke.com)