https://github.com/martincastroalvarez/django-admin-react
A drop-in React single-page admin for Django — driven entirely by your existing ModelAdmin. Dark mode, mobile cards, drag-and-drop column layout with freezable columns, keyboard a11y, JSON-aware detail rendering, and a bidirectional bridge to the classic admin during migration. Pip install and go — no React code on your side.
https://github.com/martincastroalvarez/django-admin-react
admin-dashboard admin-panel backend django django-admin django-rest frontend modeladmin python react rest-api single-page-application spa tailwindcss
Last synced: 20 days ago
JSON representation
A drop-in React single-page admin for Django — driven entirely by your existing ModelAdmin. Dark mode, mobile cards, drag-and-drop column layout with freezable columns, keyboard a11y, JSON-aware detail rendering, and a bidirectional bridge to the classic admin during migration. Pip install and go — no React code on your side.
- Host: GitHub
- URL: https://github.com/martincastroalvarez/django-admin-react
- Owner: MartinCastroAlvarez
- License: mit
- Created: 2026-05-25T10:06:59.000Z (27 days ago)
- Default Branch: main
- Last Pushed: 2026-05-29T20:35:12.000Z (22 days ago)
- Last Synced: 2026-05-30T01:20:03.839Z (22 days ago)
- Topics: admin-dashboard, admin-panel, backend, django, django-admin, django-rest, frontend, modeladmin, python, react, rest-api, single-page-application, spa, tailwindcss
- Language: TypeScript
- Homepage: https://pypi.org/project/django-admin-react/
- Size: 6.96 MB
- Stars: 10
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# django-admin-react
[](https://djangopackages.org/packages/p/django-admin-react/)
A drop-in **React single-page admin** for any Django 5+ project. Same
`pip install`, same `INSTALLED_APPS`, same `urls.py include()` — and
your `ModelAdmin` classes drive everything. No React code on your side.
```python
# settings.py
INSTALLED_APPS = [
# ...
"django.contrib.admin",
"django_admin_react", # the React SPA — includes the JSON API for you
]
# urls.py
urlpatterns = [
path("admin/", admin.site.urls),
path("admin-react/", include("django_admin_react.urls")), # SPA + API in one include
]
```
**One `INSTALLED_APPS` line + one URL include is the entire integration.** `pip install django-admin-react` transitively pulls in the [JSON API](https://pypi.org/project/django-admin-rest-api/) and the [MCP adapter](https://pypi.org/project/django-admin-mcp-api/); `django_admin_react.urls` includes the API endpoints at `/api/v1/…`, so the SPA finds its wire surface with zero configuration. (Mount the API a second time at your own prefix only if a non-SPA client also needs it.)
> **Production / Stable.** Available on PyPI; the SPA + the API
> ([`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/))
> + the MCP adapter
> ([`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/))
> all share the v1 wire contract. Track progress on the
> [Project board](https://github.com/users/MartinCastroAlvarez/projects/3)
> and the [Issues list](https://github.com/MartinCastroAlvarez/django-admin-react/issues).
## Three repos, one product
The project is split into three independently-published, cross-referenced repos so each piece can be consumed on its own merits:
| Repo | PyPI | Role |
|---|---|---|
| **[`django-admin-rest-api`](https://github.com/MartinCastroAlvarez/django-admin-api)** | [`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/) | The JSON REST API for the Django admin — same permissions, same `ModelAdmin`, no new features. The wire surface. |
| **`django-admin-react`** *(this repo)* | [`django-admin-react`](https://pypi.org/project/django-admin-react/) | The React SPA frontend. A **super-layer** that depends on `django-admin-rest-api` for every wire call. |
| **[`django-admin-mcp-api`](https://github.com/MartinCastroAlvarez/django-admin-mcp)** | [`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/) | Wire-protocol-only **MCP** adapter (call, manifest, …) over `django-admin-rest-api` — lets agents reach the same `ModelAdmin`-driven REST surface, no new functionality / permissions / validation. |
The wire contract itself lives in the **API repo** (`docs/api-contract.md` there). This README is about the SPA. The migration from "self-contained" to the 3-repo split is tracked in [META #544](https://github.com/MartinCastroAlvarez/django-admin-react/issues/544).
---
## Why django-admin-react
The Django admin is a 20-year-old hypertext app: full-page reloads,
mid-2000s aesthetics, no real mobile support, no client-side state.
It is also the most powerful piece of Django: `ModelAdmin` already
encodes your permissions, querysets, forms, fieldsets, search,
ordering, and inlines.
`django-admin-react` keeps every line of `ModelAdmin` you already
have and replaces only the UI:
| What you write | What the React SPA does with it |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| `list_display` | Renders columns in a virtualised, sortable, mobile-collapsing table. |
| `search_fields` | Renders a search bar that hits `get_search_results` verbatim. |
| `list_filter` | Renders a sidebar drawer (desktop) / bottom-sheet (mobile) + filter chips. |
| `date_hierarchy` | Renders a year → month → day drill-down strip. |
| `list_editable` / `list_per_page` | Renders inline-editable cells + paginated list with deep links. |
| `actions` | Renders a bulk-actions menu wired to the same `ModelAdmin.actions`. |
| `fieldsets` / `readonly_fields` | Renders the detail form respecting groups + read-only rules. |
| `autocomplete_fields` | Renders type-ahead pickers that hit `/autocomplete/?q=…`. |
| `inlines = [TabularInline, ...]` | Renders inlines as tables / card stacks alongside the parent. |
| `has_*_permission` | Hides Add / Save / Delete buttons accordingly; never invents a permission. |
| `get_queryset(request)` | Every list, search, and detail lookup starts here. Never `Model.objects.all()`. |
The SPA is **metadata-driven** — it learns your models, fields, and
permissions at runtime from `GET /api/v1/registry/`. Add a new
`ModelAdmin` and refresh; no rebuild, no codegen.
---
## Screenshots
Real captures of the **django-admin-react SPA** rendering the bundled
`examples/` apps — driven entirely by each app's `ModelAdmin`.
### Light + dark — your `ModelAdmin` decides the chrome, the theme is operator/user choice
| Registry / home (dark) | List view — `list_display` + filters + actions |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|  |  |
| List view (dark) | Detail view |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|  |  |
| Detail view (dark) | Sign in (package login) |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|  |  |
### Phone-shaped (375 px) — `RecordCardList` fallback, full feature parity
| Mobile list (cards) | Mobile detail (stacked fieldsets) |
| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|  |  |
### One API, many surfaces
The SPA is one consumer of the wire format. The same JSON powers the
React app, the [MCP layer](https://pypi.org/project/django-admin-mcp-api/),
and any client you write:

Screenshots are captured deterministically against the
[`examples/`](./examples) apps' fixtures — no real names, emails,
account numbers, or PII.
---
## Install
```bash
pip install django-admin-react
```
This pulls in the JSON API ([`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/))
and the MCP adapter ([`django-admin-mcp-api`](https://pypi.org/project/django-admin-mcp-api/))
as transitive dependencies. The **two-line `INSTALLED_APPS` + one-line
URL include** at the top of this README is the *entire* integration.
Mount at any prefix you like — `/admin-react/`, `/staff/`,
`/back-office/` — just don't collide with `django.contrib.admin`'s
own mount.
Log in as a staff user → modern, Tailwind-styled SPA driven by your
existing `ModelAdmin` classes.
The wheel ships the **pre-built React bundle**. You do **not** need
Node, pnpm, or any frontend toolchain to install or run.
### Optional configuration
All settings are optional. Defaults shown:
```python
DJANGO_ADMIN_REACT = {
"ADMIN_SITE": "django.contrib.admin.site", # dotted path to AdminSite instance
"DEFAULT_PAGE_SIZE": 25, # fallback only; the list page size derives
# from ModelAdmin.list_per_page (Django parity).
"MAX_PAGE_SIZE": 200,
"ENABLE_PROFILING": False,
# Branding — all optional. The defaults derive from your AdminSite
# (site_header / site_title / site_logo), so if you already branded
# the HTML admin you need nothing here. Rendered server-side into the
# SPA shell, so title + favicon are present on first paint (no FOUC).
"BRAND_TITLE": None, # str | None — override for BOTH brand strings.
"BRAND_LOGO_URL": None, # str | None — favicon + sidebar logo;
# falls back to AdminSite.site_logo. Absolute
# URL or a path under your STATIC_URL.
"PRIMARY_COLOR": "#2563eb", # accent for primary buttons, links, and
# active states. Hex only (validated);
# injected as the --dar-primary CSS var, so
# rebranding needs no React rebuild.
# Auth + API mount
"REACT_LOGIN": True, # bool — React-rendered login is the default;
# the SPA shell is served to anonymous users
# and posts to /api/v1/login/. Set False to
# opt back into the legacy admin HTML login.
"API_URL_PREFIX": None, # str | None — point the SPA at a separately-
# mounted django-admin-rest-api (e.g.
# "/api/api/v1/"). Default None keeps the
# inline include the package ships today.
}
```
#### Branding (`BRAND_TITLE` + `BRAND_LOGO_URL`)
Both default to `None` and **derive from your `AdminSite`**, mirroring
Django admin — so if you already customised the HTML admin's branding,
you need no settings here at all.
**Sidebar header** resolution:
1. `DJANGO_ADMIN_REACT["BRAND_TITLE"]` — explicit override.
2. `.site_header` — reused automatically.
3. `"Django Admin"` — last-resort fallback.
**Browser-tab ``** resolution (Django uses `site_title` for the
tab, `site_header` for the on-page header):
1. `DJANGO_ADMIN_REACT["BRAND_TITLE"]` — explicit override.
2. `.site_title` — Django's tab-title source.
3. `.site_header` — fallback.
4. `"Django Admin"` — last-resort fallback.
`BRAND_LOGO_URL` accepts either an absolute URL or a path the browser
can resolve under your `STATIC_URL`. When unset, a `site_logo` attribute
on your `AdminSite` is used (Django has no logo by default, so set it as
a constant on your custom site). It is used both as the favicon
(`` in the SPA shell) and as the small logo next to
the brand title in the sidebar.
```python
# settings.py
DJANGO_ADMIN_REACT = {
"BRAND_TITLE": "Acme",
"BRAND_LOGO_URL": "/static/acme/logo.svg",
}
```
Both values are written into the SPA index template as standard
`` tags (`dar-brand-title`, `dar-brand-logo`); the React shell
reads them at boot, so the first paint already carries the consumer's
brand. No flash of the package's defaults.
### Requirements
- **Python**: 3.10+
- **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
- **Database**: anything Django supports — the package is ORM-only,
no direct SQL.
- **Auth**: Django's built-in session + CSRF. Works with custom
`AUTH_USER_MODEL`, custom `AUTHENTICATION_BACKENDS`, and custom
`AdminSite.has_permission`.
### Production: static files (and media for file uploads)
The wheel ships the pre-built bundle under the package's `static/` and
serves it through `{% static %}`. With `DEBUG = True`, Django's
staticfiles app serves it automatically — nothing to do. **In
production** you collect + serve static files like any Django app:
```python
# settings.py
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles" # where collectstatic gathers files
```
```bash
python manage.py collectstatic --no-input
```
Then serve `STATIC_ROOT` from your web server / CDN — or let
[WhiteNoise](https://whitenoise.readthedocs.io/) do it:
```python
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware", # right after SecurityMiddleware
# ...
]
```
> If the SPA shell loads but its JS/CSS 404 (blank page, console errors),
> this `collectstatic` step is what's missing.
**File / image fields.** Editing `FileField` / `ImageField` needs
Django's media settings:
```python
# settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
```
Uploads go through your configured file storage
(`STORAGES["default"]` / `DEFAULT_FILE_STORAGE`); in production serve
`MEDIA_ROOT` from your web server or object storage as usual.
> ⚠️ **Serving user-uploaded media has security implications** (access-gating, stored-file XSS). See [`SECURITY.md` §9](SECURITY.md) before exposing `MEDIA_URL` in production — `FileField`/`ImageField` are writable.
### Running side-by-side with the legacy admin
A common rollout: keep `/admin/` on the legacy HTML admin, mount the
React SPA at `/admin-react/`, and migrate users at your own pace.
Both run off the same `ModelAdmin` registrations — there is no
duplicate state.
```python
urlpatterns = [
path("admin/", admin.site.urls), # legacy, unchanged
path("admin-react/", include("django_admin_react.urls")), # SPA
]
```
#### Experience-toggle strip (optional)
During the rollout, show a thin **persistent** strip at the top of
every page on **both** admins that links to the same page on the
other admin. Users can switch surfaces in one click, regardless of
which one they're on:
```python
# settings.py
DJANGO_ADMIN_REACT = {
"LEGACY_ADMIN_URL_PREFIX": "admin/", # the legacy admin's mount
"REACT_ADMIN_URL_PREFIX": "admin2/", # this package's mount
}
```
Both values must match the prefixes you used in `urls.py`. When set:
- The **React SPA** renders a strip linking the same path under the
legacy admin's mount (with `?query=string` preserved and a trailing
slash, since Django admin URLs require one).
- The **legacy Django admin** renders the mirror strip linking the
matching React URL.
Set `LEGACY_ADMIN_URL_PREFIX` alone if you only want the SPA → legacy
direction (reverse direction stays off).
##### `INSTALLED_APPS` ordering
For the legacy-side strip, list `django_admin_react` **before**
`django.contrib.admin`. Django's template loader resolves
`admin/base_site.html` left-to-right and the first match wins —
the package's override of that template injects the strip:
```python
INSTALLED_APPS = [
"django_admin_react", # ← BEFORE django.contrib.admin
"django.contrib.admin",
# ...
]
```
If you don't enable the legacy-side strip (`REACT_ADMIN_URL_PREFIX`
unset) the ordering doesn't matter — the override is a no-op for
consumers who haven't opted in.
##### UX contract
The strip is **subtle and persistent**: one line tall, neutral
chrome, no dismiss control. Operators turn it on/off via the
settings; end-users do not. When you remove the settings (or set
them to `None`), the strips disappear on the next page load —
completing the migration.
---
## Extend without writing React
Everything below is **just `ModelAdmin`**. No JavaScript. No new
classes. The UI follows whatever your admin declares.
### Pick what columns appear on the list view
```python
@admin.register(Invoice)
class InvoiceAdmin(admin.ModelAdmin):
list_display = ("number", "customer", "status", "total", "issued_at")
```
### Make columns sortable
```python
class InvoiceAdmin(admin.ModelAdmin):
list_display = ("number", "customer", "status", "total", "issued_at")
sortable_by = ("issued_at", "total") # everything else is fixed
```
### Add free-text search
```python
class InvoiceAdmin(admin.ModelAdmin):
search_fields = ("number", "customer__name", "notes__icontains")
# The SPA wires `?q=` to `ModelAdmin.get_search_results` verbatim.
```
### Default ordering
```python
class InvoiceAdmin(admin.ModelAdmin):
ordering = ("-issued_at",)
```
### Hide a field from the form
```python
class InvoiceAdmin(admin.ModelAdmin):
exclude = ("internal_audit_hash",) # never reaches the SPA
readonly_fields = ("total",) # rendered as read-only
```
The SPA respects `exclude` and `readonly_fields` exactly the way the
legacy admin does. Sensitive-named fields (`password`, `secret`,
`token`, `api_key`, `hash`, `private_key`, `session`, `nonce`, `salt`)
are filtered on top of those rules as defense-in-depth.
### Group fields into sections
```python
class InvoiceAdmin(admin.ModelAdmin):
fieldsets = (
("Identity", {"fields": ("number", "customer")}),
("Money", {"fields": ("subtotal", "tax", "total")}),
("Lifecycle", {"fields": ("status", "issued_at", "paid_at")}),
("Internal", {"fields": ("notes",), "classes": ("collapse",)}),
)
```
### Surface filters in the sidebar
```python
class InvoiceAdmin(admin.ModelAdmin):
list_filter = ("status", "issued_at", "customer")
# Boolean / choices / FK / date / SimpleListFilter all supported.
```
### Drill down by date
```python
class InvoiceAdmin(admin.ModelAdmin):
date_hierarchy = "issued_at"
# SPA renders a year → month → day strip wired to ?year=&month=&day=
```
### Edit cells inline on the list view
```python
class InvoiceAdmin(admin.ModelAdmin):
list_editable = ("status",)
# SPA: click cell → input swap → blur/Enter saves via PATCH ///bulk/
```
### Add custom admin actions
Declare actions the stock-Django way; the SPA surfaces them in both places automatically. **One `@admin.action` declaration → two surfaces:** the changelist multi-select dropdown **and** a per-object button on the detail page. No `django-object-actions`, no `change_actions = (...)` redeclaration, no parameter-name gymnastics:
```python
class InvoiceAdmin(admin.ModelAdmin):
actions = ("mark_paid",)
@admin.action(description="Mark selected as paid")
def mark_paid(self, request, queryset):
queryset.update(status="paid", paid_at=timezone.now())
```
That single declaration shows up in the changelist's bulk-actions dropdown (operating on every selected row) **and** as a button on the detail page (operating on the single visible row, dispatched as a one-row queryset).
#### Optional: detail-only actions
If you want an action to render **only** on the detail page (not in the changelist dropdown), give its third parameter a single-object shape — name it `obj_id` / `object_id` / `pk` / `id` / `object_pk`, or annotate it `str` / `int` / a `Model` subclass. The API's signature classifier (api 1.0.6+) marks those as `target: "detail"`:
```python
class InvoiceAdmin(admin.ModelAdmin):
actions = ("mark_paid", "regenerate_pdf")
@admin.action(description="Mark selected as paid")
def mark_paid(self, request, queryset):
... # changelist + detail (batch shape)
@admin.action(description="Regenerate PDF")
def regenerate_pdf(self, request, obj_id: str):
invoice = self.model.objects.get(pk=obj_id)
invoice.regenerate_pdf()
# detail page only — the stock Django changelist runner
# expects a queryset, so this shape won't run from there.
```
Classifier rules (api 1.0.6+):
| Third parameter | Target | Where it renders |
|---|---|---|
| name `queryset` / `qs`, or `QuerySet` annotation | `batch` (default) | Changelist multi-select **and** detail page |
| name `obj_id` / `object_id` / `pk` / `id` / `object_pk` | `detail` | Detail page only |
| annotation `str` / `int` / `Model` subclass | `detail` | Detail page only |
| anything else | `batch` (default, preserves stock Django) | Changelist multi-select **and** detail page |
Same `@admin.action` decorator regardless. Same `ModelAdmin.actions` tuple. Same audit trail. The signature picks the surface; the default surfaces on both.
### Per-row permission gating
```python
class InvoiceAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
return request.user.has_perm("billing.create_invoice")
def has_change_permission(self, request, obj=None):
if obj is None:
return request.user.has_perm("billing.change_invoice")
return obj.owner_id == request.user.id # row-level rule
def has_delete_permission(self, request, obj=None):
return False # nobody deletes invoices
def has_view_permission(self, request, obj=None):
return request.user.has_perm("billing.view_invoice")
```
The SPA hides the **Add** / **Save** / **Delete** buttons automatically
based on these. UI never invents a permission; it asks `ModelAdmin`.
### Restrict the queryset
```python
class InvoiceAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(owner=request.user)
```
The list view never sees rows the queryset excludes. **No
`Model.objects.all()` in the package** — every list, search, and
detail lookup starts at `ModelAdmin.get_queryset(request)`.
### Custom save hook
```python
class InvoiceAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.last_edited_by = request.user
super().save_model(request, obj, form, change)
```
Writes always go through `ModelAdmin.get_form()` → `form.is_valid()`
→ `save_model()`. Signals, audit logs, and post-save hooks all fire
exactly like they do in `/admin/`.
### Use a custom `AdminSite`
```python
# myproject/admin.py
from django.contrib.admin import AdminSite
class StaffAdminSite(AdminSite):
site_header = "Operations Console"
site_title = "Ops"
index_title = "Welcome"
def has_permission(self, request):
return request.user.is_active and request.user.is_staff and \
request.user.groups.filter(name="ops").exists()
staff_admin = StaffAdminSite(name="staff")
# myproject/settings.py
DJANGO_ADMIN_REACT = {
"ADMIN_SITE": "myproject.admin.staff_admin",
}
```
The SPA inherits the custom site's permission gate and the
`ModelAdmin` registrations on that site — no parallel registry.
### Plug in custom field types
```python
# yourapp/admin_react.py
from django_admin_react.api.serializers import register_field_type
from yourapp.fields import MoneyField
register_field_type(MoneyField, vocab_type="decimal")
# SPA renders MoneyField with the built-in decimal widget; no React
# code required.
```
Coining a brand-new `vocab_type` (with a matching SPA widget) is an
**API-repo** concern — open the issue at
[`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api).
### Pre-built `get_*` overrides still work
`get_form`, `get_fieldsets`, `get_fields`, `get_exclude`,
`get_readonly_fields`, `get_search_results`, `get_list_display`,
`get_sortable_by`, `get_list_filter`, `get_actions` — all of them
are called by the SPA the same way the HTML admin calls them. If
you customised them for `/admin/`, the SPA already honours those
customisations.
---
## Feature status
All three packages are **Production / Stable** on PyPI. The
`ModelAdmin`-driven REST API + the React SPA + the MCP adapter
all share the v1 wire contract. Per-feature live status below.
| `ModelAdmin` surface | Backend (REST API) |
| ------------------------------------------------------ | --------------------------------------------------------------- |
| Registry / list / detail / create / update / delete | ✅ |
| `list_display`, `sortable_by`, `search_fields` | ✅ |
| `list_filter` (boolean / choice / FK / date / Simple) | ✅ |
| `date_hierarchy` | ✅ |
| `list_editable` + bulk PATCH | ✅ |
| `actions` — batch + detail (signature-classified) | ✅ |
| `autocomplete_fields` / `raw_id_fields` | ✅ |
| `ManyToManyField` read + write | ✅ |
| `inlines` (TabularInline / StackedInline) — read + write | ✅ |
| `FileField` / `ImageField` — read | ✅ |
| `FileField` / `ImageField` — multipart upload | 🟡 [#241](https://github.com/MartinCastroAlvarez/django-admin-react/issues/241) |
| `JSONField` / `ArrayField` / range — read | ✅ |
| range fields — write coercion | 🟡 [#238](https://github.com/MartinCastroAlvarez/django-admin-react/issues/238) |
| `register_field_type` + per-model extension hook | ✅ |
| React login / logout (Django session + CSRF) | ✅ |
| Password set / change (`UserAdmin` parity) | ✅ |
| Session-expiry re-login contract | ✅ |
| OpenAPI 3.1 schema at `/api/v1/schema/` | ✅ |
| PWA manifest + service worker (cache-purge on logout) | ✅ |
✅ = shipped. 🟡 = not yet built (tracked).
---
## The API surface
The SPA is a thin client over a small, closed REST surface. You can
also use these endpoints from any HTTP client (curl, your own
frontend, a script).
| Method | Path | Purpose |
| ------- | ------------------------------------------------- | ----------------------------------------------------------------------------- |
| `GET` | `/api/v1/registry/` | All apps + models the current user can see, with their permissions. |
| `GET` | `/api/v1/schema/` | OpenAPI 3.1 schema for the envelopes + closed type vocabulary. |
| `GET` | `/api/v1///` | Paginated list. Honours `?search=`, `?ordering=`, `?page=`, `list_filter`. |
| `POST` | `/api/v1///` | Create. Runs `ModelAdmin.get_form()` + `form.is_valid()` + `save_model()`. |
| `GET` | `/api/v1////` | Detail with serialised fields, `permissions`, `inlines`, `panels`. |
| `PATCH` | `/api/v1////` | Partial update. Same form pipeline as POST. |
| `DELETE`| `/api/v1////` | Hard delete via `ModelAdmin.delete_model()`. |
| `PATCH` | `/api/v1///bulk/` | `list_editable` round-trip for multiple rows. |
| `POST` | `/api/v1////` | Invoke a registered `ModelAdmin.actions` entry on a queryset. |
| `GET` | `/api/v1///autocomplete/?q=…` | `autocomplete_fields` lookup. Permission-gated on the **target** model. |
Every endpoint is **staff-only by default** (or whatever
`AdminSite.has_permission` returns), CSRF-required on unsafe
methods, and emits `Cache-Control: no-store`. Full wire contract
lives in the API repo:
[`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api).
---
## Examples
Six runnable example projects ship with the repo under
[`examples/`](examples/):
| Project | What it exercises |
| ---------- | -------------------------------------------------------------------------------------------------- |
| `library/` | `Author`, `Book`, `Genre` — basic CRUD, FKs, M2M, `search_fields`, `list_filter`. |
| `fintech/` | `Account`, `Transaction` — permissions, queryset narrowing, custom actions. |
| `blog/` | `Post`, `Tag`, `Comment` — `list_editable`, `inlines`, `date_hierarchy`. |
| `ecommerce/` | `Product`, `Order`, `LineItem` — fieldsets, readonly, `register_field_type` for `MoneyField`. |
| `hr/` | `Employee`, `Department` — `autocomplete_fields`, `raw_id_fields`, organisational filters. |
| `project/` | Glue project that mounts every example app for an end-to-end demo. |
Boot any of them with:
```bash
cd examples/project
python manage.py migrate
python manage.py loaddata seed
python manage.py runserver
# → http://127.0.0.1:8000/admin/ (legacy admin)
# → http://127.0.0.1:8000/admin-react/ (the React SPA)
```
---
## What you get
- **Plug-and-play**: works with any `ModelAdmin` you already have.
- **Shared auth**: Django sessions, CSRF, staff permissions. No new
user model, no parallel permission system.
- **Responsive, modern UI**: React + Tailwind + React Query, served
as a single bundle from `django_admin_react/static/admin_react/`.
- **Extensible by editing `ModelAdmin`**, not React. Per-model SPA
extension hooks for the cases that genuinely need them.
- **Configurable URL prefix** — `/admin/`, `/admin-react/`, anywhere.
- **Conservative & secure-by-default** — never exposes models the
admin doesn't already expose; never writes fields the admin form
excludes; CSRF on every unsafe method; `Cache-Control: no-store`
on every API response; sensitive-name denylist on top of the
admin's own `exclude` rules.
- **Boring + auditable** — no parallel permission system, no
client-side workarounds for backend permissions, conservative
serializer with `str()` fallback.
---
## License
MIT — see [`LICENSE`](LICENSE).
## Security
Please report security issues privately through GitHub's Private
Vulnerability Reporting on the repository (Security → Advisories).
See [`SECURITY.md`](SECURITY.md). Do **not** open a public issue.
## Contributing
Open an [Issue](https://github.com/MartinCastroAlvarez/django-admin-react/issues/new)
or a [Discussion](https://github.com/MartinCastroAlvarez/django-admin-react/discussions)
before sending a PR for anything non-trivial. **API-side contributions** (any
`/api/v1/...` endpoint, the wire contract, permission gates, serializer
denylist) go to [`MartinCastroAlvarez/django-admin-api`](https://github.com/MartinCastroAlvarez/django-admin-api)
— this repo owns only the **React SPA super-layer** on top.