Skip to main content

Command Palette

Search for a command to run...

Authorization in Django: From Permissions to Policies: Part 4 — Convention as Architecture

Understanding How Django Establishes Permissions

Updated
4 min read
Authorization in Django: From Permissions to Policies: Part 4 — Convention as Architecture

By this point in the series, several foundations are already in place.

  • Permissions are not logic. They are data.

  • ContentTypes exist to provide stable, database-level model identity.

  • Authorization in Django depends on identifiers, not Python objects or runtime checks.

Once those pieces are accepted, the next question is:

If permissions are plain data, and ContentTypes identify models, who decides which permissions exist at all?

It is not about which permissions are granted or how they are enforced, but which permission identifiers are allowed to exist in the system in the first place.

This part answers that question.

The answer is not “the developer,” and it is not “the admin interface.” It is the framework itself—through convention.

The Architectural Problem Django Had to Solve

Authorization systems live or die on identifier stability.

For a permission system to function across an ecosystem, permission identifiers must satisfy several constraints simultaneously:

  • They must exist before any enforcement logic runs

  • They must be predictable, so multiple subsystems can reference them

  • They must be stable across environments, deployments, and time

  • They must be shared, without requiring coordination between consumers

Django’s authorization surface is consumed by many independent components:

  • has_perm() checks

  • Template conditionals

  • The admin site

  • Django REST Framework

  • Third-party packages

  • Internal tooling

All of these rely on the same permission strings.

A system where permissions are defined manually, dynamically, or locally would require constant coordination. Every consumer would need to know which permissions exist, how they are named, and when they are created. That does not scale.

This is not a developer-experience problem, but a system design constraint.

Convention-Based Permission Generation

Django solves this problem by removing choice.

For every concrete model, Django defines a fixed set of permissions by convention:

  • add_<model>

  • change_<model>

  • delete_<model>

  • view_<model>

These permissions are:

  • Derived mechanically from model schema

  • Created during migrations

  • Stored as rows in the database

  • Available before any application logic runs

There is no runtime inference.
There is no implicit registry.
There is no per-app negotiation.

What looks like convenience is actually architectural discipline.

Convention is doing real work here: it transforms schema into identity.

Why These Four Permissions Exist

The choice of these four permissions is deliberate. They represent the minimal, generic interaction surface that can apply to any model, regardless of domain.

  1. Create

  2. Read

  3. Update

  4. Delete

Django does not attempt to encode business meaning into these permissions. There is no concept of approval, publishing, ownership, or workflow state. Those ideas are domain-specific and unstable.

The addition of view reflects the recognition that read access is a distinct capability, but it remains generic. It does not imply who may view or when viewing is allowed.

These permissions describe capability, not policy.

That distinction is critical. Capability defines what actions exist. Policy defines when they are valid.

Django only commits to the former.

Naming as a Stability Contract

Permission codenames are not just labels. They are contracts.

Once created, these identifiers are referenced across:

  • Code

  • Database rows

  • Migrations

  • Third-party integrations

  • Deployment environments

Because permission names include both the app label and the model name, structural changes directly affect authorization.

Renaming a model is not a cosmetic change. Changing an app label is equally significant. In both cases, the permission identity changes.

Django does not try to repair this automatically. It cannot determine whether a rename represents the same concept or a different one. That decision belongs to the system’s architecture, not the framework.

This follows the same rule as ContentTypes: identity is defined explicitly, not inferred.

The Closed Loop: Convention → Migration → Enforcement

Django’s authorization system forms a closed loop:

  • Models define schema

  • Conventions derive permission identifiers

  • Migrations materialize them as data

  • Enforcement tools consume them uniformly

There is no hidden registry and no runtime discovery.

The authorization surface is defined once, materialized as data, and consumed uniformly.

The database is the single source of truth.

Every consumer—admin, templates, DRF, or custom code—reads from the same canonical permission rows. This is what allows Django’s authorization system to remain simple, predictable, and extensible without coordination.

Django’s minimalism is deliberate.

It does not provide:

  • A role system

  • Ownership or object-level semantics

  • State-aware permissions

  • Workflow or lifecycle enforcement

  • Context-dependent authorization

Embedding any of these would couple permission identity to business logic, making the system brittle and unstable.

Instead, Django offers a stable authorization substrate: a small, fixed set of generic capabilities with durable identifiers. Higher-level meaning is intentionally left to policy layers built on top.

This is not a missing feature. It is what allows the core to remain stable and scalable.

Where This Goes Next (Part 5 Preview)

Permissions are no longer abstract ideas. They exist as concrete rows in real tables, tied to ContentTypes and enforced through queries. The next step is to look at that data directly. Part 5 examines django_content_type and auth_permission as they actually exist in the database—real rows, real relationships, and their architectural implications. Django authorization is not driven by runtime logic, but by data, conventions, and contracts.

Part 5 is where those contracts become visible.

More from this blog

A

Abhilash PS — Engineering Thought & Software Architecture

23 posts