skip to content

For a SaaS product where customers sign in by email, how do you define a Django custom user model with USERNAME_FIELD and BaseUserManager?

level: middleimportance: should knowfreq 52%

answer

  1. no username column at all
  2. which field identifies the account
  3. createsuperuser prompts from a list
  4. manager builds users and superusers
  5. domain case only is normalised

basics

~10 s

Subclass AbstractBaseUser and PermissionsMixin, declare a unique EmailField, set USERNAME_FIELD = 'email' and REQUIRED_FIELDS for other mandatory fields, and write a BaseUserManager subclass whose create_user and create_superuser take email instead of username.

solid answer

~40 s

Start from `AbstractBaseUser` plus `PermissionsMixin` so there is no `username` column. Declare `email = models.EmailField(unique=True)`; the default `ModelBackend` requires the `USERNAME_FIELD` to be unique (check `auth.E003`). Set `USERNAME_FIELD = 'email'` and `EMAIL_FIELD = 'email'`, and list other non-blank fields such as a workspace name in `REQUIRED_FIELDS`, never the email or password. Add `is_staff` and an `is_active` field. The manager subclasses `BaseUserManager`: `create_user(email, password=None, **extra)` normalises the address with `normalize_email()`, calls `set_password()` and saves; `create_superuser` forces `is_staff` and `is_superuser` to `True`. Point `AUTH_USER_MODEL` at it before the first migrate. Remember that `normalize_email()` lowercases only the domain, so decide how you handle case in the local part.

code

python · 45 lines
python
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils import timezone


class AccountManager(BaseUserManager):
    use_in_migrations = True

    def _create(self, email, password, **extra_fields):
        if not email:
            raise ValueError('An email address is required.')
        user = self.model(email=self.normalize_email(email), **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', False)
        extra_fields.setdefault('is_superuser', False)
        return self._create(email, password, **extra_fields)

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')
        return self._create(email, password, **extra_fields)


class Account(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    workspace_name = models.CharField(max_length=100)
    full_name = models.CharField(max_length=150, blank=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField(default=True)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = AccountManager()

    USERNAME_FIELD = 'email'
    EMAIL_FIELD = 'email'
    REQUIRED_FIELDS = ['workspace_name']

go deeper

for a junior

Remember the pieces: a unique EmailField, USERNAME_FIELD set to it, and a manager with create_user and create_superuser.

for a middle

Explain who reads each attribute: ModelBackend and get_by_natural_key read USERNAME_FIELD, only createsuperuser reads REQUIRED_FIELDS, and the E002/E003 checks guard both.

for a senior

Raise the email case problem unprompted and pick a policy for it, applied on every write path rather than only at sign-up.

for a principal

Treat the login key as a product contract: changing addresses, merged accounts and case policy all hang off USERNAME_FIELD, so decide them before the first migration.

## The requirement A SaaS product usually identifies customers by the address they signed up with. Nobody wants to invent a username, and support staff search by email. Django's default `User` makes `username` the login key and treats `email` as an optional, non-unique field, so an email-first product needs its own user model built on the minimal base. ## The model Build on **`AbstractBaseUser`** (password and `last_login` only) and **`PermissionsMixin`** (`is_superuser`, `groups`, `user_permissions`). Then declare what the product needs: - `email = models.EmailField(unique=True)` - the login key; - `full_name`, a `workspace_name` or whatever the sign-up form collects; - `is_staff` and `is_active` as `BooleanField`s, because `PermissionsMixin` does not add them and the admin reads both; - `date_joined` if you want it, since only `AbstractUser` provides it. Three class attributes tell `django.contrib.auth` how to treat the model: | Attribute | Value here | What reads it | |---|---|---| | `USERNAME_FIELD` | `'email'` | `get_username()`, `natural_key()`, `BaseUserManager.get_by_natural_key()`, `ModelBackend.authenticate()`, `createsuperuser` | | `EMAIL_FIELD` | `'email'` | `get_email_field_name()`, used for example by password reset; defaults to `'email'` if absent | | `REQUIRED_FIELDS` | `['workspace_name']` | only `createsuperuser`, which prompts for each listed field | The system checks enforce the rules: `auth.E002` if `USERNAME_FIELD` appears in `REQUIRED_FIELDS`, and `auth.E003` if the username field is not unique while `AUTHENTICATION_BACKENDS` is the default `ModelBackend` alone (with other backends it downgrades to warning `auth.W004`). The docs add that `REQUIRED_FIELDS` must include every field whose `blank` is `False`, and that it has no effect elsewhere, for instance on user creation in the admin. ## The manager `createsuperuser` and your sign-up code both go through the default manager, so it must know the new shape. `UserManager` assumes a `username` argument, so you subclass **`BaseUserManager`**, which already gives you: - `normalize_email()` - strips surrounding whitespace and lowercases the **domain part only**; - `get_by_natural_key()` - a `get()` on `USERNAME_FIELD`, which `ModelBackend` uses to find the account. You add two methods whose signatures take the username field plus the required fields: 1. `create_user(email, password=None, **extra_fields)` - reject a missing email, normalise it, build `self.model(...)`, call `set_password()` so the value is hashed, and save with `using=self._db`. 2. `create_superuser(email, password=None, **extra_fields)` - default `is_staff` and `is_superuser` to `True` and raise `ValueError` if a caller overrides them, mirroring `UserManager`. `createsuperuser` calls `create_superuser(**user_data)` with the `USERNAME_FIELD`, `password` and each `REQUIRED_FIELDS` entry as keyword arguments, so the parameter names must match the field names. Setting `use_in_migrations = True` on the manager makes it available inside data migrations. ## The case-sensitivity decision Because `normalize_email()` leaves the local part alone and `get_by_natural_key()` is an exact match, `[email protected]` and `[email protected]` can become two accounts, and a customer who types a different capitalisation cannot sign in. Most products decide at the model layer: - lowercase the whole address in `create_user` and wherever the email is changed, accepting that a few mail systems treat the local part as case-sensitive; or - keep the stored case, override `get_by_natural_key()` with an `email__iexact` lookup, and enforce case-insensitive uniqueness with a constraint on `Lower('email')` in addition to `unique=True`, which `auth.E003` still expects on the field. Whichever you pick, apply it in every write path, including profile edits. ## Common mistakes in this model - **Starting from `AbstractUser` and only setting `USERNAME_FIELD = 'email'`.** The `username` column is still there, still unique and still required by `UserManager.create_user()`, so sign-up code must invent a value for it. Setting `username = None` on the subclass removes the field, but the default manager then breaks, so you end up writing the manager anyway. - **Forgetting `is_staff`.** Superusers created from the command line cannot open the admin if the model has no staff flag the admin can read. - **Assigning the raw password.** Writing `user.password = raw` stores plain text; only `set_password()` hashes. - **Listing optional fields in `REQUIRED_FIELDS`.** The command then insists on a value the product treats as optional. ## Wiring it in Set `AUTH_USER_MODEL = 'accounts.Account'` before the first `migrate`, and create the model in the app's first migration. From then on the rest of the code uses `settings.AUTH_USER_MODEL` and `get_user_model()` and never cares that there is no username.

  • Why must the email not appear in REQUIRED_FIELDS?
    `createsuperuser` always prompts for the `USERNAME_FIELD` and the password, so listing the email again would duplicate the prompt. Django's system check `auth.E002` rejects a model whose `USERNAME_FIELD` is inside `REQUIRED_FIELDS`, with a hint to remove it.
  • What happens if the email field is not unique?
    With the default `AUTHENTICATION_BACKENDS` (only `ModelBackend`) the system check `auth.E003` fails, because `get_by_natural_key()` must find exactly one account. With a custom backend configured Django downgrades this to warning `auth.W004`, and that backend must handle duplicates itself.

saying these in an interview costs you the question

  • Setting USERNAME_FIELD = 'email' on AbstractUser removes the username column
  • REQUIRED_FIELDS should list the email and password fields
  • normalize_email() lowercases the entire address
  • The default UserManager accepts email in place of username
  • REQUIRED_FIELDS makes the admin's add-user form require those fields