The short version
Django's auth system is 9 tables: auth_user, auth_group, auth_permission, django_content_type, django_admin_log, and three join tables (auth_user_groups, auth_user_user_permissions, auth_group_permissions). The permission system is built on content types, each model gets a default add, change, delete, and view permission. See the full interactive version in the Django auth schema diagram.
The core tables
auth_user
| Column | Type | What it means |
|---|---|---|
id | int (PK) | Auto-incrementing primary key. |
password | varchar(128) | Hashed password (PBKDF2 by default). |
last_login | datetime | When the user last logged in. |
is_superuser | bool | Bypasses all permission checks. |
username | varchar(150) | Unique username. |
email | varchar(254) | Email address. |
is_staff | bool | Can access the Django admin. |
is_active | bool | Set to False instead of deleting users. |
auth_group
| Column | Type | What it means |
|---|---|---|
id | int (PK) | Auto-incrementing ID. |
name | varchar(150) | Unique group name (e.g., "Editors"). |
auth_permission
| Column | Type | What it means |
|---|---|---|
id | int (PK) | Auto-incrementing ID. |
name | varchar(255) | Human-readable name (e.g., "Can add post"). |
content_type_id | int (FK) | Which model this permission applies to. |
codename | varchar(100) | Code identifier (e.g., add_post). |
django_content_type
Maps every model in your project to an ID. The permission system uses it to know which model a permission applies to.
How permissions work
A permission is tied to a content type (a model). Groups collect permissions. Users are added to groups to inherit their permissions. You can also assign permissions directly to users.
Which migration creates which tables
The tables arrive in layers as you migrate. The contenttypes app migration creates django_content_type first, every model in your project gets one row. Then the auth app's initial migration creates auth_user, auth_group, auth_permission, and the three join tables. The admin app adds django_admin_log last. If you only need users without groups, you still get all of them, Django does not split the auth migrations.
The contenttypes trick
django_content_type is the clever bit: it maps (app_label, model) pairs to integer IDs, so permissions attach to models rather than tables. A permission row says "can change X" where X is a content type ID; the same mechanism powers generic foreign keys elsewhere in Django. This is why adding a new model and running migrations silently creates four new permission rows, one per default action.
A permission check in one query
To list everything user 42 can do, join through both paths, group memberships and direct grants:
SELECT DISTINCT p.codename
FROM auth_permission p
JOIN django_content_type ct ON ct.id = p.content_type_id
LEFT JOIN auth_group_permissions gp ON gp.permission_id = p.id
LEFT JOIN auth_user_groups ug ON ug.group_id = gp.group_id AND ug.user_id = 42
LEFT JOIN auth_user_user_permissions up ON up.permission_id = p.id AND up.user_id = 42
WHERE ug.user_id = 42 OR up.user_id = 42;
Note the two join tables doing different jobs: auth_user_groups carries role-like membership, while auth_user_user_permissions carries one-off grants. Most RBAC designs, see the RBAC schema, separate these the same way.
FAQ
Does Django create all these tables automatically?
Yes. Running python manage.py migrate creates all auth tables, contenttypes first, then auth, then admin.
How is this different from NextAuth or Laravel auth?
Django's permissions attach to models via content types, which neither NextAuth nor Laravel do. Compare the NextAuth schema (OAuth accounts and sessions, no permissions) and the Laravel schema (sessions and jobs, no permission tables at all).
What's the difference between is_superuser and is_staff?is_superuser bypasses all permission checks. is_staff only controls access to the Django admin.
Can I use Django without the permission system?
Yes. Remove django.contrib.auth from INSTALLED_APPS.