[Answered ]-Django – multiple users with unique data

1👍

Have you thought of subdomains?

Custom subdomains are a great way to provide customization for customers of SaaS products and to differentiate content without resorting to long URL paths.

So when a company registers, you either take the company name call to_lower() method, or prompt them to choose the one they like, just like Slack does, or Jira. I suggest you read this article Using Subdomains in Django Applications

👤Dan

1👍

It depends on how you want to store data in you SaaS application – single db for all instances or multiple db (each instance have separate database). Multiple db approach is pain when you want add new features, migrations etc. Single db is easier to manage but you need to add bunch of ForeignKey for each model.

For single db you will need:

  1. Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
  2. Database router that will return database name for read/write depending on SaaS instance.

That’s all. Django will read/write to separate databases.

For multiple db you will need:

  1. Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).

Because you probably don’t want to add ForeignKey to each model manually and filter it manually:

  1. Abstract model with ForeignKey and custom save method to auto set that ForeignKey.
  2. Custom model manager with custom get_queryset method that will filter all ORM queries with current SaaS instance. This manager should overide create method to auto set ForeignKey for queries like this: Foo.objects.create(**data)

Each model that will be fitlered for SaaS instance should inherit from that abstract model and you will need to set this model manager to that custom model manager.

That’s all. Django will filter you ORM queries for current SaaS instance.

Example middleware (uses Domain model to check if domain exists, if not you will get HTTP404):

try:
    from threading import local
except ImportError:
    from django.utils._threading_local import local

_thread_locals = local()

def get_current_saas_instance():
    return getattr(_thread_locals, 'current_instance', None)

class SaaSSubdomainMiddleware(object):
    def process_request(self, request):
        _thread_locals.current_instance = None
        host = request.get_host()

        try:
            domain = Domain.objects.get(name=host)
            _thread_locals.current_instance = domain.company
        except:
            logger.error('Error when checking SaaS domain', exc_info=True)
            raise Http404

Example abstract model:

class SaaSModelAbstract(Model):
    SAAS_FIELD_NAME = 'company'
    company = ForeignKey(Company, null=True, blank=True)

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        from .middleware import get_current_saas_instance
        self.company = get_current_saas_instance()
        super(SaaSModelAbstract, self).save(*args, **kwargs)

Example model manager:

class CurrentSaaSInstanceManager(models.Manager):
    def get_current_saas_instance(self):
        from .middleware import get_current_saas_instance
        return get_current_saas_instance()

    def get_queryset(self):
        current_instance = self.get_current_saas_instance()

        if current_instance is not None:
            return super(CurrentSaaSInstanceManager, self).get_queryset().filter(
                **{self.model.SAAS_FIELD_NAME: current_instance})

        return super(CurrentSaaSInstanceManager, self).get_queryset()

    def create(self, **kwargs):
        current_instance = self.get_current_saas_instance()

        if current_instance is not None:
            kwargs[self.model.SAAS_FIELD_NAME] = current_instance

        instance = self.model(**kwargs)
        self._for_write = True
        instance.save(force_insert=True, using=self.db)
        return instance

Example models:

class FooModel(SaaSModelAbstract):
    # model fields, methods 

    objects = CurrentSaaSInstanceManager()

class BarModel(models.Model):
    # model fields, methods
    pass

Example queries:

FooModel.objects.all() # will return query with all objects for current SaaS instance
BarModel.objects.all() # will return all objects withoout SaaS filtering

# Create objects for SaaS instance:
FooModel.objects.create(**data)
# or:
foo = FooModel()
foo.save()

In both cases (single/multiple db) django admin will be working properly.

I’m not posted db router because implementation is trivial and all you need can be found in django docs.

0👍

You can surely use django-tenant-schemas

Edit:

Given that you mentioned in the comment to my original answer that the database being used is MySQL, django-tenant-schemas is useless in your case. How about using multiple databases with database routers, that ways there can be separate databases for every company and using database routers you can route request for your databases through it.

It might be overwork but you probably can figure out a slick way to do it.

Leave a comment