Building a User Model
The user model is the canonical first document in most applications, and it exercises nearly everything in this tutorial: field types and constraints, uniqueness enforced by indexes, hashing for secrets, a scoped queryset for the "active" definition, lifecycle stamps via signals, and a reference that other models will point at. This chapter assembles all of it into one coherent model - the template to copy, adjust, and build on.
The Model
import datetime
from mongoengine import (Document, StringField, BooleanField, ListField,
DateTimeField, EmailField)
class User(Document):
username = StringField(required=True, unique=True, min_length=3, max_length=30,
regex=r"^[a-z0-9_]+$")
email = EmailField(required=True, unique=True)
display_name = StringField(max_length=80)
password_hash = StringField(required=True)
active = BooleanField(default=True)
is_staff = BooleanField(default=False)
roles = ListField(StringField(), default=list)
deleted_at = DateTimeField(default=None)
created_at = DateTimeField(default=datetime.datetime.now(datetime.timezone.utc))
updated_at = DateTimeField(default=datetime.datetime.now(datetime.timezone.utc))
meta = {
"collection": "users",
"indexes": ["email", "username", "deleted_at"],
"ordering": ["-created_at"],
"strict": True,
}
Notes on the choices. username and email are unique=True - the guarantee comes from the indexes (ensure_indexes()), not from the ODM. password_hash is a plain string holding salt-plus-hash, never the password itself. roles is a list of strings - flexible for small role sets, with a constant defined at the app level rather than an EnumField when roles evolve per deployment. strict = True keeps the stored document and this class in sync. The model stores no raw secrets.
Password Hashing
import hashlib
import os
def hash_password(password: str) -> str:
salt = os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 200_000)
return (salt + dk).hex()
def verify_password(password: str, stored: str) -> bool:
raw = bytes.fromhex(stored)
salt, dk = raw[:16], raw[16:]
candidate = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 200_000)
return candidate == dk
The pattern: a random per-user salt, a slow KDF (PBKDF2 with 200k iterations as a floor - tune the cost against a 200-500 ms budget), and the salt stored alongside the hash. In production, prefer a battle-hardened library (argon2-cffi or bcrypt) over hand-rolled PBKDF2; the shape is identical, and the verification must be constant-time (the libraries provide it). The model's job is to store the hash string; the KDF choice is an application decision that must be versioned if you ever want to rehash on login.
Lifecycle with Signals
from mongoengine import pre_save
@pre_save
def touch_user(sender, document, created, **kwargs):
if sender is not User:
return
document.updated_at = datetime.datetime.now(datetime.timezone.utc)
if not created:
document.created_at = document.created_at or document.updated_at
The pre_save hook keeps updated_at honest on every write without any call site remembering it - the same signal idiom from the signals chapter, scoped to this class.
The Scoped Queryset
from mongoengine.queryset.manager import Manager
from mongoengine.queryset.queryset import QuerySet
class UserQuerySet(QuerySet):
def active(self):
return self.filter(active=True, deleted_at=None)
def staff(self):
return self.filter(is_staff=True, active=True, deleted_at=None)
class UserManager(Manager):
queryset_class = UserQuerySet
# add to the model:
# objects = UserManager()
Every read path in the application goes through User.objects.active() (or staff()), so "which users are visible" is one definition. The authenticated-user lookup becomes a one-liner with the index on username:
def authenticate(username: str, password: str):
user = User.objects.active().get_or_none(username=username)
if user and verify_password(password, user.password_hash):
return user
return None
Being the Referenced Side
class Post(Document):
title = StringField()
author = ReferenceField(User, required=True)
meta = {"indexes": ["author"]}
# deleting a user must handle her posts - the cascade from the signals chapter
from mongoengine import pre_delete
@pre_delete
def reassign_user_posts(sender, document, **kwargs):
if sender is not User:
return
Post.objects(author=document, deleted_at=None).update(set__deleted_at=
datetime.datetime.now(datetime.timezone.utc))
The user model is where the rest of the data model's references converge - posts, comments, orders all point at User. That makes the delete policy a first-class design: soft-delete the posts (as above), reassign them, or block the delete while posts exist. Deciding it now, in the model's own module, is what keeps it from being discovered in production.
Summary
Build the user model as: constrained unique fields backed by indexes, a salted slow-hash string for passwords (library-provided in production), strict mode, updated_at stamped by a pre_save signal, and a scoped active() queryset as the single definition of visibility. Then design the reference side explicitly - the cascade or reassignment policy for every collection that points at the user - because a user model is the hub of your data graph, and its delete behavior is the system's behavior.