Concepts¶
This page explains the ideas behind django-tenant-options – why it works the way it does, and how the pieces fit together. Read this before diving into the how-to guides if you want to understand the “why” behind the design decisions.
The problem¶
In a SaaS application, tenants (organizations, teams, accounts) often need to customize form options. A project management tool might need different task priorities for different companies. An HR system might need different job title categories per department.
The naive approaches each have drawbacks:
CharField with TextChoices: One fixed set of options for everyone. Adding options requires code changes and deployments.
ManyToManyField with a custom model: No built-in distinction between global defaults and tenant-specific options. Complex to manage.
JSON fields: No schema validation, no referential integrity, can’t query efficiently.
Custom tables per tenant: Schema complexity, migration nightmares, performance issues.
django-tenant-options solves this by providing a structured, flexible system where developers define global defaults and tenants can customize what’s available to their users.
The two-model pattern¶
Every set of customizable options in django-tenant-options is defined by a pair of models:
Option model – Stores every available option: mandatory defaults, optional defaults, and tenant-created custom options.
Selection model – A through model that records which options each tenant has currently enabled.
Why two models instead of one? Separation of concerns. The Option model is the catalog of everything that could be chosen. The Selection model tracks what each tenant has actually chosen. This makes it straightforward to:
Add new default options without affecting existing tenant selections
Let tenants enable/disable optional defaults independently
Let tenants create custom options that only they can see
Soft-delete options without breaking existing records that reference them
Option types¶
Every option has one of three types:
Mandatory (OptionType.MANDATORY)¶
Always available to all tenants. Cannot be disabled. Every tenant’s users will see these options in forms.
Use for: Core options that every tenant needs. In a project management tool, “High” and “Low” priorities might be mandatory – every team needs at least these.
Optional (OptionType.OPTIONAL)¶
Available to all tenants, but each tenant decides whether to enable it. A tenant admin can select or deselect these through a selections form.
Use for: Common options that not every tenant needs. “Critical” priority might be useful for some teams but unnecessary clutter for others.
Custom (OptionType.CUSTOM)¶
Created by a specific tenant for their own use. Only visible to the tenant that created it. Other tenants never see it.
Use for: Tenant-specific needs you can’t anticipate. One company might need a “Blocked by Legal” status. Another might want an “Awaiting Client Feedback” priority.
How options flow through the system¶
Here’s the lifecycle, from developer to end user:
Developer defines defaults – You specify
default_optionson your Option model with names and types (mandatory or optional).Database sync – Running
python manage.py syncoptionscreates the actual database records for these defaults. Run this after migrations or whenever you changedefault_options.Tenant admin manages selections – A tenant admin uses a
SelectionsFormto choose which optional defaults to enable. Mandatory options are always included automatically. They can also create custom options through anOptionCreateFormMixinform.End user sees filtered options – When an end user fills out a form using
UserFacingFormMixin, the form automatically shows only the options that the user’s tenant has selected. Mandatory options, enabled optional options, and the tenant’s custom options all appear. Nothing else.
Soft deletes¶
Both Option and Selection models use soft deletes. When you “delete” an option, it isn’t removed from the database – instead, a deleted timestamp is set on the record.
This matters for data integrity. If a task has a “Critical” priority and that priority option gets deleted, the task still has a valid foreign key pointing at the option record. Without soft deletes, you’d face cascading deletes or broken references.
.delete()sets thedeletedtimestamp (soft delete).delete(override=True)performs an actual database deletion (hard delete).undelete()clears thedeletedtimestamp, restoring the recordQuerySets provide
.active()and.deleted()filters
The metaclass system¶
When you inherit from AbstractOption or AbstractSelection, custom metaclasses automatically add fields and relationships to your model:
A
tenantForeignKey pointing to your tenant modelAn
associated_tenantsManyToManyField (on Option models) through the Selection modelAn
optionForeignKey (on Selection models) pointing to the paired Option model
You don’t need to understand the metaclass internals. Just know that when you set tenant_model and selection_model (or option_model) on your concrete models, the metaclasses use those to wire up the relationships. This is why those class attributes are required.
Managers and querysets¶
The package provides custom managers and querysets with methods tailored to common operations:
OptionManagerwith methods likecreate_for_tenant(),create_mandatory(), andcreate_optional()OptionQuerySetwith methods likeoptions_for_tenant(),selected_options_for_tenant(),active(), anddeleted()An
unscopedmanager on every model that returns all records, including soft-deleted ones
Your concrete models automatically get these managers. If you define custom managers, they must inherit from OptionManager or SelectionManager – otherwise the package’s filtering won’t work. Django system checks will warn you if this isn’t set up correctly.
Database triggers¶
For defense in depth, django-tenant-options can generate database triggers that enforce tenant-option consistency at the database level. This prevents mismatches that could occur from direct SQL queries or bugs that bypass Django’s validation layer.
Triggers are optional but recommended for production. Generate them with python manage.py maketriggers, then apply with python manage.py migrate.
Further reading¶
Terminology – Precise definitions for all key terms
Models Guide – How to define Option and Selection models
Forms Guide – How to build forms that respect tenant selections
Configuration Reference – All available settings