django-adr
A Django-native decision log for software architecture — a structured, queryable database of Architectural Decision Records (ADR) that lives inside your application rather than in loose Markdown files.
Requires Python 3.12 or newer and Django 5.2 LTS or newer. Every supported combination — Python 3.12/3.13/3.14 against Django 5.2/6.0/6.1 — is exercised by the test suite on every push.
Features
- Django Admin interface to create and manage ADRs
- HTML list and detail views with status filtering
- Supersession lineages — every ADR links to the decisions it replaced and the one that replaced it, with a timeline view across the whole log
- Read-only REST API (Django REST Framework) with pre-rendered Markdown HTML fields, status filtering, and a per-ADR supersession chain endpoint
- Management command
create_adrto create ADRs from the CLI, with optional--supersedesto mark an existing ADR as superseded in one step - Management command
export_adrsto export all ADRs as Markdown files, optionally with YAML front matter for MkDocs, Sphinx, or Docusaurus - Markdown support for context, decision, and consequences fields, escaped by default
- Internationalization (i18n) support — English locale included
Installation
pip install django-adr
Add to INSTALLED_APPS:
INSTALLED_APPS = [
...
"django_adr",
]
Include the URLs:
from django.urls import include, path
urlpatterns = [
...
path("adrs/", include("django_adr.urls", namespace="django_adr")),
]
Run migrations:
python manage.py migrate
Usage
Admin
Visit /admin/django_adr/adr/ to manage ADRs through the Django admin.
HTML views
/adrs/— list all ADRs/adrs/?status=accepted— filter by status/adrs/<number>/— view a single ADR, including its supersession history/adrs/timeline/— every supersession lineage, grouped
REST API
GET /adrs/api/adrs/— list all ADRsGET /adrs/api/adrs/?status=accepted— filter by statusGET /adrs/api/adrs/<number>/— retrieve a single ADRGET /adrs/api/adrs/<number>/chain/— the ADR's full supersession lineage, oldest decision first
The API is unpaginated by default, so it inherits whatever the host project configures:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 50,
}
Management commands
Create a new ADR:
python manage.py create_adr "Use PostgreSQL" \
--context="We need a relational database." \
--decision="Use PostgreSQL." \
--consequences="Team must know SQL."
Create a new ADR and supersede an existing one in a single step:
python manage.py create_adr "Use CockroachDB" --supersedes=3
This creates the new ADR and automatically marks ADR-0003 as Superseded.
Export all ADRs as Markdown files:
python manage.py export_adrs --output-dir=docs/adr
Each ADR is written to <output-dir>/<number>-<slug>.md.
Add --front-matter to prefix each file with a YAML block, so the export drops
straight into a docs-as-code pipeline:
python manage.py export_adrs --output-dir=docs/adr --front-matter
---
adr: 0003
title: "Use PostgreSQL"
status: accepted
date: 2026-08-21
superseded_by: 0012
---
ADR statuses
| Status | Description |
|---|---|
proposed |
Under discussion |
accepted |
Agreed and in effect |
deprecated |
No longer relevant |
superseded |
Replaced by a newer ADR |
rejected |
Considered and not adopted |
Protecting the views
The HTML views and REST API are public by default. The package does not enforce any authentication strategy — that is left to the host project.
HTML views — login_required cannot wrap an include(), because include()
returns a URLconf rather than a view. Either protect the whole project with
LoginRequiredMiddleware (Django 5.1+):
MIDDLEWARE = [
...
"django.contrib.auth.middleware.LoginRequiredMiddleware",
]
or subclass the views and route them yourself:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import path
from django_adr.views import ADRDetailView, ADRListView, ADRTimelineView
class ProtectedADRListView(LoginRequiredMixin, ADRListView):
pass
class ProtectedADRDetailView(LoginRequiredMixin, ADRDetailView):
pass
class ProtectedADRTimelineView(LoginRequiredMixin, ADRTimelineView):
pass
app_name = "django_adr"
urlpatterns = [
path("", ProtectedADRListView.as_view(), name="adr-list"),
path("timeline/", ProtectedADRTimelineView.as_view(), name="adr-timeline"),
path("<int:number>/", ProtectedADRDetailView.as_view(), name="adr-detail"),
]
REST API — set DEFAULT_PERMISSION_CLASSES in settings.py:
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
}
Or scope it to the ADR router only by subclassing ADRViewSet:
from django_adr.api import ADRViewSet
from rest_framework.permissions import IsAuthenticated
class ProtectedADRViewSet(ADRViewSet):
permission_classes = [IsAuthenticated]
Markdown rendering
ADR bodies are rendered with mistune and
raw HTML in the source is escaped, so an ADR body cannot inject markup into
the HTML views or the API. Link schemes such as javascript: and data: are
neutralised by the renderer.
If every author of an ADR is trusted and you need raw HTML to pass through, opt out explicitly:
DJANGO_ADR_MARKDOWN_ESCAPE = False
Anyone who can write an ADR can then execute script in the browser of anyone who reads one.
Translations
All user-facing strings are translatable. The package ships with an English locale. To generate translations for your project:
python manage.py makemessages -l it
Ensure USE_I18N = True in your project settings.
License
MIT
Metadata
Release files for django-adr 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_adr-2.0.0.tar.gz | 44.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_adr-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.6 kB
Release files / django_adr-2.0.0.tar.gz
| Download URL | django_adr-2.0.0.tar.gz |
|---|---|
| Size | 44.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0bd5c6ded23372a587e57e86ea3c55e92210d69797b0ab69e2bf23eef601d7a
|
|
BLAKE2b-256 checksum How to use checksums |
5d4172d4c6f4c0464570a657138c75a8a594582b711881563a8196e17000a6c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / django_adr-2.0.0-py3-none-any.whl
| Download URL | django_adr-2.0.0-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2ef82af7743a0e35881b4a2da3932c7eaa726230058bfb0f0804df05b7971368
|
|
BLAKE2b-256 checksum How to use checksums |
3af8c743cbbfd755a769de0ff08b669a65e6590851554f2adf51b18629bfc0c3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|