Back to blog
By Rasul

Django Custom User Model: A Practical Guide

Create a Django custom user model before the first migration, separate login policy from profile data, and plan changes to existing accounts.

For a new application, create a Django custom user model by extending AbstractUser, setting AUTH_USER_MODEL, and creating its initial migration before migrating the database. For an existing application, first separate the requested change into identity schema, login policy, or product profile data. Only the first necessarily calls for replacing the user model.

This tutorial gives you a minimal new-project setup and a decision process for applications that already have accounts. It uses Django 6.0 documentation and source observations from the hosted Djass application as of September 16, 2026. The examples are implementation guidance, not a report of a migration run.

The short version

  1. Decide which part of the account system must change.
  2. For a new database, define a small custom user before the first migration.
  3. Point relationships and application lookups at the configured user model.
  4. Configure login behavior separately from the database identifier.
  5. Put product-specific state in explicit domain models.
  6. Treat an existing user-table replacement as its own migration project.

On this page: choose the boundary · create the model · reference users · set login policy · place product state · existing accounts · worked example · FAQ

Step 1: Choose the account boundary

Start with the user-visible outcome. “Let customers sign in with email” and “move every account to a different database model” are not the same project. Write the requested outcome without naming a model class, then classify it.

Requested outcome Boundary to investigate first What should remain stable
Email instead of username on the login screen Authentication provider and login policy Existing account IDs and ownership
Store timezone or onboarding preferences Profile or preferences model Authentication behavior
Track a subscription or billing customer Billing domain model Login identifier and password handling
Change the fields that define an account User schema Documented account mapping
Give one person different workspace roles Membership and authorization One person’s authentication identity

This identity, login, product-state split is useful when a starter repository already exists. It stops a small feature request from turning into a database rewrite, while leaving room for a custom user where it is justified.

The hosted Djass application provides a concrete example. Its Profile model links to Django’s built-in User and holds Stripe references and product lifecycle state. Its account settings configure email and username login through allauth. Email-oriented account flows therefore coexist with the built-in user table.

That observation describes the hosted Djass service, not every repository produced by the external django-saas-starter template. Inspect your generated project’s settings and models before adapting an example. The repository-structure guide helps you locate the relevant application boundaries.

Done looks like: one sentence identifying the requested change, its owning component, and the account identifiers that must not change.

Step 2: Create a Django custom user model

Use this path for a genuinely new Django project whose initial migrations have not been applied. If your starter already has a user model or migration history, continue to Step 6 instead of creating a second identity model.

From the directory containing manage.py, create an app:

python manage.py startapp accounts

Define the smallest useful user class in accounts/models.py:

from django.contrib.auth.models import AbstractUser


class User(AbstractUser):
    pass

Add accounts to the existing INSTALLED_APPS list. Keep the normal Django admin, authentication, content types, sessions, messages, and static-files apps. Then set:

AUTH_USER_MODEL = "accounts.User"

The value uses the application label and model name, not a dotted Python module path. Django’s AUTH_USER_MODEL setting reference documents this contract and warns against changing it after database tables exist. This setup retains username-based identity; it does not remove the username field or make email unique.

Register the model in accounts/admin.py:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User

admin.site.register(User, UserAdmin)

Now create and apply migrations:

python manage.py makemigrations accounts
python manage.py migrate
python manage.py createsuperuser

Django’s custom-authentication documentation recommends a custom user for new projects and requires the swappable user model in its app’s first migration. AbstractUser preserves the standard user implementation; AbstractBaseUser provides a lower-level starting point with more integration work. Prefer the former unless your identity contract needs the latter.

Keeping the initial class empty is intentional. You gain an application-owned model without inventing fields for billing, organizations, invitations, and future features that may never belong there.

Done looks like: accounts.User is created by accounts/0001_initial.py, the new database is migrated, and the account appears through the configured admin. Adding custom fields later also requires updating the relevant forms and admin fieldsets.

Step 3: Reference the configured user model

A model replacement only helps if other code respects that boundary. Use settings.AUTH_USER_MODEL for model relationships and get_user_model() for runtime access. Those are Django’s documented reference mechanisms; avoid a direct import of the concrete built-in User in reusable application code.

For example, an app that owns customer projects can define:

from django.conf import settings
from django.db import models


class CustomerProject(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name="customer_projects",
    )
    name = models.CharField(max_length=120)

PROTECT is a deliberate policy in this example: deleting an account should not silently delete its projects. Choose deletion behavior from your product’s retention and ownership rules, not from whichever snippet you copied first.

A service that needs the active class can resolve it at runtime:

from django.contrib.auth import get_user_model


def find_active_account(account_id):
    return get_user_model().objects.get(pk=account_id, is_active=True)

This lookup establishes account state, not permission to access a particular project. A calling view still needs to scope project access to the requester or an authorized workspace membership.

For a migration containing data transformations, use the migration’s historical app registry rather than importing today’s model. Django’s migration documentation explains why historical model state must remain usable when old migrations are replayed. Runtime indirection and migration history solve different problems.

Done looks like: the project has a consistent reference convention, and relationship deletion rules reflect the product rather than a default habit.

Step 4: Separate login policy from user storage

Before removing username, decide whether you actually need to remove it. A customer can experience an email-first signup while the application keeps a username internally. That is the boundary visible in hosted Djass’s settings and account adapter.

The allauth account configuration reference separately documents login methods, signup fields, and user-model field names. These settings must agree with your model, but changing a login screen does not by itself require a new user table.

Write an identity contract before changing either layer:

  • What stable ID do projects, invoices, and audit events reference?
  • Which values may a person enter to authenticate?
  • Can an email address change without creating a different account?
  • When does the application trust a new address?
  • How are ambiguous or duplicate identifiers handled?
  • Which component owns normalization and uniqueness?

Do not treat email = models.EmailField(unique=True) as the complete contract. You still need a policy for case handling, verification, address changes, and all entrypoints that create accounts. A manager, signup form, import job, and social-login adapter should not each invent different identity rules.

The Django allauth tutorial covers the signup, verification, social-login, and recovery lifecycle. This article’s concern is which stored account those flows reach, not repeating their installation.

Done looks like: changing a verified customer’s contact address does not accidentally transfer project ownership, billing history, or access to a second account.

Step 5: Give product state an explicit owner

A custom user and a profile are not mutually exclusive. Keep the account model focused on identity; assign product state to the component that owns its lifecycle. This is an architectural recommendation, not a requirement to split every optional field into a new table.

Consider the consequences before choosing placement. If a billing integration changes, should an authentication model migration be necessary? If a customer leaves one workspace, should their login disappear? If onboarding is restarted, should their identity be recreated? Those questions usually reveal whether fields describe the person, a membership, or a commercial relationship.

Hosted Djass makes this separation visible in code:

Repository observation Transferable lesson
Profile owns Stripe references and lifecycle state Product state can evolve outside the authentication table
Project references its user owner Ownership must survive any identity migration
ProfileStateTransition records lifecycle history separately Current state and historical evidence have different lifetimes
A user-creation signal creates the profile Related-record creation is an explicit application responsibility

These are source observations, not evidence that the arrangement is the best choice for every SaaS. A profile adds another relationship to maintain. A small application may reasonably keep a few identity-adjacent fields on a custom user. The important choice is an explicit owner and deletion policy.

Django’s authentication-customization documentation notes that profile models are ordinary related models, not automatically created account extensions. If you choose one, decide what happens during imports, administrative account creation, and recovery from a missing profile. Avoid making an unrelated analytics or billing outage prevent identity creation unless that dependency is an intentional product requirement.

Done looks like: each field has one authoritative owner, and missing optional product state has a defined recovery path.

Step 6: Plan changes to an existing user model

Do not apply the new-project commands to a populated database as a retrofit. Django explicitly describes mid-project user-model substitution as manual schema and data work. A changed setting is not an account migration plan.

Build an identity dependency map before deciding the scope:

Dependency Question to resolve
Foreign keys and many-to-many tables Which records must keep pointing to the same person?
Account and social-provider records How will each provider identity reach its original account?
Groups, permissions, and admin access Which privileges must be preserved or deliberately removed?
Sessions and background jobs Can old identifiers or serialized task arguments outlive cutover?
Billing, exports, and audit history Which external references require a stable mapping?
Forms, managers, signals, and direct imports Which code still assumes the old class or field set?

Hosted Djass shows why both schema and runtime code belong on that map. Its historical migrations use swappable dependencies, while several current models and signal handlers still refer to the concrete built-in User. Other relationships use settings.AUTH_USER_MODEL. A migration file containing a swappable dependency is therefore not proof that the whole application is ready for a user-model replacement.

For an unavoidable change, prepare a project-specific sequence: inventory references, define the old-to-new account mapping, resolve identifier collisions, migrate dependent data, control writes during cutover, and specify how to recover if ownership or access diverges. Rehearse with protected data in an isolated environment. The database-backup guide explains why a recovery plan needs a demonstrated restore path.

Keep the scope honest. Adding a profile to store preferences is different from replacing a user table. Keeping IDs stable is different from keeping table names stable. Preserving row counts is different from preserving which customer owns which project. The acceptance criteria should express these distinctions.

Done looks like: a migration plan names every dependent system, a cutover owner, and the exact conditions that require recovery. There is no universal safe SQL recipe for an unknown identity graph.

Worked example: a SaaS account with projects

Imagine a new product that needs username-based admin access, customer projects, and future email-oriented signup. This is an illustrative design, not a claim about a Djass customer or a tested deployment.

Start with the empty accounts.User subclass from Step 2 and the CustomerProject relationship from Step 3. Keep the user’s primary key as the internal account reference. Configure customer login separately when you add allauth; do not use a mutable email address as the project’s owner reference.

Next, write down three outcomes: one account can own multiple projects; changing its email leaves those projects attached; attempted account deletion cannot silently remove them. These outcomes explain the foreign key and PROTECT choice more clearly than a long list of hypothetical user fields.

If you later add billing, create the billing relationship that matches the payer. A personal product might bill an account. A team product might bill a workspace. Putting a subscription directly on User before making that choice can create a second migration problem unrelated to authentication.

When adapting a generated repository, begin with the available generator modules and existing models. Djass provides a maintained generation workflow; you still own these identity and lifecycle decisions. See Djass pricing if that repository starting point fits your workflow.

Common mistakes

  • Replacing identity to change a login field. Investigate provider policy before committing to a database migration.
  • Deleting migration history to make an error disappear. Shared databases still depend on that history; diagnose the actual dependency graph.
  • Using a profile as a permission check. A profile’s existence does not prove project or workspace access.
  • Copying concrete User imports everywhere. New code should respect the configured model boundary.
  • Choosing AbstractBaseUser for unspecified future flexibility. List the actual identity requirement that AbstractUser cannot express first.
  • Assuming generated code and the hosted generator have identical models. Read the repository you will deploy.

Django custom user model FAQ

How do I get the user model in Django?

Use django.contrib.auth.get_user_model() for the active model class. For a model relationship, reference settings.AUTH_USER_MODEL. Inside a historical data migration, resolve the model through that migration’s app registry.

What are the fields in the Django user model?

The default user includes username, password, first name, last name, email, staff and active flags, superuser status, last login, and join date, with group and permission relationships. The Django auth reference describes these fields. A custom model may expose a different set.

How do I create a user in Django?

Use the configured model’s manager and its create_user() method, supplying the fields that manager requires. Use createsuperuser for an administrative account. Django’s authentication guide documents user creation; do not assign a raw password directly to the password field.

Can I change AUTH_USER_MODEL after migrations?

Not as a settings-only change. Existing relationships, data, and migration history need a deliberate migration plan. If the requested feature only needs profile data or a different login method, evaluate those smaller changes first.