Run a Django backend (Django ORM, Django Admin) alongside a Reflex app.
Project description
reflex-django
Run Django and Reflex in one process โ one command, zero glue.
๐ Full Documentation ยท GitHub ยท PyPI
reflex-django is a Reflex plugin that boots your Django ASGI app and your Reflex app side-by-side in a single process under reflex run. HTTP paths like /admin, /api, and /static go straight to Django. Everything else โ the Reflex SPA and the live WebSocket event channel โ stays on Reflex.
Table of Contents
- Why reflex-django?
- Quick Install
- Django Settings Configuration
- Wire it into rxconfig.py
- Accessing the Logged-In User with AppState
- Simple CRUD Without Mixins
- Architecture Overview
- Commands
- What's Next?
Why reflex-django?
Reflex sends UI events over WebSocket, not normal HTTP requests. This means Django's session middleware, authentication, and locale detection don't run for Reflex events by default.
reflex-django fixes this with an event bridge that:
- Reconstructs a synthetic
HttpRequestfrom WebSocket cookies and headers on every event. - Loads the Django session and resolves
request.userautomatically. - Exposes
self.requestdirectly inside your Reflex state classes.
You get Django's full ORM, Admin, auth, and migrations โ plus Reflex's reactive UI โ without running two separate servers.
| Django | Python |
|---|---|
| 6.0.x | 3.12+ |
Quick Install
# 1. Create a project and add dependencies
uv init
uv add reflex reflex-django
# 2. Scaffold the Reflex frontend
uv run reflex init frontend
# 3. Create a Django project
uv run django-admin startproject backend .
# 4. Run!
uv run reflex run
Django Settings Configuration
Open backend/settings.py and make sure the following are configured. These are the minimum settings needed for reflex-django to work correctly.
# backend/settings.py
INSTALLED_APPS = [
# Django built-ins (required)
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# reflex-django helpers (required)
"reflex_django",
# Your own apps
"myapp",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", # required for sessions
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "backend.urls"
# Database โ SQLite for local dev, swap for PostgreSQL in production
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Sessions โ stored in the database by default (required for the event bridge)
SESSION_ENGINE = "django.contrib.sessions.backends.db"
# Static files
STATIC_URL = "/static/"
# Optional: built-in auth pages (login, register, password reset)
REFLEX_DJANGO_AUTH = {
"SIGNUP_ENABLED": True,
"LOGIN_URL": "/login",
"LOGIN_REDIRECT_URL": "/dashboard",
}
Tip: Run migrations after updating
INSTALLED_APPS:uv run reflex django migrate
Wire it into rxconfig.py
Tell Reflex where your Django settings live by passing settings_module to ReflexDjangoPlugin:
# rxconfig.py
import reflex as rx
from reflex_django import ReflexDjangoPlugin
config = rx.Config(
app_name="frontend",
plugins=[
ReflexDjangoPlugin(
settings_module="backend.settings",
# Route these HTTP paths to Django:
admin_prefix="/admin", # Django Admin (default)
backend_prefix="/api", # Your REST/HTTP views (optional)
),
],
)
That's it. reflex run now boots both frameworks together.
Accessing the Logged-In User with AppState
AppState is the recommended base class for any state that needs to know who is logged in. It binds self.request (a proxy to the synthetic Django HttpRequest) on every WebSocket event, giving you the authenticated user, session, and query params.
# frontend/state.py
import reflex as rx
from reflex_django.state import AppState
class DashboardState(AppState):
"""Example state that reads the logged-in user."""
greeting: str = ""
@rx.event
async def load_greeting(self):
# self.request.user is the real Django User object โ use it for
# permissions, ownership checks, and any server-side logic.
if not self.request.user.is_authenticated:
return rx.redirect("/login")
username = self.request.user.get_username()
self.greeting = f"Welcome back, {username}!"
@rx.event
async def save_preference(self, theme: str):
# Read/write the Django session directly
self.request.session["theme"] = theme
await self.request.session.asave()
# frontend/pages/dashboard.py
import reflex as rx
from frontend.state import DashboardState
def dashboard_page() -> rx.Component:
return rx.vstack(
rx.heading(DashboardState.greeting),
rx.button("Load", on_click=DashboardState.load_greeting),
)
# app.add_page(dashboard_page, route="/dashboard", on_load=DashboardState.load_greeting)
AppState at a glance
| Inside event handlers | For UI components (rx.cond, etc.) |
|---|---|
self.request.user โ live Django User object |
self.is_authenticated โ bool var |
self.request.session โ read/write session data |
self.username, self.email โ string vars |
self.request.GET โ query string params |
self.user_id โ int var |
await self.has_perm("app.action") โ permission check |
Auto-synced on every event |
await self.login(username, password) |
|
await self.logout() |
Security: Always check
self.request.user.is_authenticated(orawait self.has_perm(...)) inside event handlers before reading or mutating data. Client-side state vars are for display only.
Simple CRUD Without Mixins
This example shows how to build a full Create, Read, Update, Delete task manager using plain AppState and async Django ORM โ no mixins, no code generation, no magic. This is the most transparent and customizable approach.
1. The Model
# myapp/models.py
from django.conf import settings
from django.db import models
class Task(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="tasks",
)
title = models.CharField(max_length=200)
done = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
uv run reflex django makemigrations myapp
uv run reflex django migrate
2. The State
# frontend/state.py
import reflex as rx
from reflex_django.state import AppState
from myapp.models import Task
class TaskState(AppState):
# --- reactive vars synced to the browser ---
tasks: list[dict] = []
title: str = ""
editing_id: int = -1
error: str = ""
# โโ helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _require_user(self):
"""Return the logged-in user, or raise a redirect."""
if not self.request.user.is_authenticated:
raise PermissionError("login required")
return self.request.user
async def _serialize_tasks(self, qs) -> list[dict]:
"""Turn a queryset into a plain list of dicts for the UI."""
return [
{"id": t.id, "title": t.title, "done": t.done}
async for t in qs
]
# โโ CRUD event handlers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@rx.event
async def load_tasks(self):
"""Load all tasks for the current user."""
self.error = ""
try:
user = self._require_user()
except PermissionError:
return rx.redirect("/login")
qs = Task.objects.filter(user=user)
self.tasks = await self._serialize_tasks(qs)
@rx.event
async def create_task(self):
"""Create a new task from the title input."""
self.error = ""
if not self.title.strip():
self.error = "Title cannot be empty."
return
try:
user = self._require_user()
except PermissionError:
return rx.redirect("/login")
await Task.objects.acreate(user=user, title=self.title.strip())
self.title = ""
return TaskState.load_tasks
@rx.event
async def start_edit(self, task_id: int):
"""Populate the input for editing an existing task."""
task = await Task.objects.aget(pk=task_id, user=self.request.user)
self.title = task.title
self.editing_id = task_id
@rx.event
async def save_edit(self):
"""Persist the edited title."""
self.error = ""
if not self.title.strip():
self.error = "Title cannot be empty."
return
await Task.objects.filter(
pk=self.editing_id,
user=self.request.user,
).aupdate(title=self.title.strip())
self.title = ""
self.editing_id = -1
return TaskState.load_tasks
@rx.event
async def toggle_done(self, task_id: int):
"""Flip the done flag on a task."""
task = await Task.objects.aget(pk=task_id, user=self.request.user)
task.done = not task.done
await task.asave(update_fields=["done"])
return TaskState.load_tasks
@rx.event
async def delete_task(self, task_id: int):
"""Permanently delete a task."""
await Task.objects.filter(
pk=task_id,
user=self.request.user,
).adelete()
return TaskState.load_tasks
@rx.event
def cancel_edit(self):
"""Discard the current edit."""
self.title = ""
self.editing_id = -1
3. The Page
# frontend/pages/tasks.py
import reflex as rx
from frontend.state import TaskState
def task_row(task: dict) -> rx.Component:
return rx.hstack(
rx.checkbox(
checked=task["done"],
on_change=TaskState.toggle_done(task["id"]),
),
rx.text(
task["title"],
text_decoration=rx.cond(task["done"], "line-through", "none"),
flex="1",
),
rx.button("Edit", on_click=TaskState.start_edit(task["id"]), size="1"),
rx.button(
"Delete",
on_click=TaskState.delete_task(task["id"]),
color_scheme="red",
size="1",
),
width="100%",
align="center",
)
def tasks_page() -> rx.Component:
return rx.container(
rx.heading("My Tasks", size="5", margin_bottom="4"),
# Error banner
rx.cond(
TaskState.error != "",
rx.callout(TaskState.error, color_scheme="red", margin_bottom="3"),
),
# Create / edit form
rx.hstack(
rx.input(
value=TaskState.title,
on_change=TaskState.set_title,
placeholder="What needs doing?",
flex="1",
),
rx.cond(
TaskState.editing_id >= 0,
rx.hstack(
rx.button("Save", on_click=TaskState.save_edit),
rx.button("Cancel", on_click=TaskState.cancel_edit, variant="soft"),
),
rx.button("Add", on_click=TaskState.create_task),
),
width="100%",
margin_bottom="4",
),
# Task list
rx.vstack(
rx.foreach(TaskState.tasks, task_row),
width="100%",
spacing="2",
),
max_width="600px",
padding="6",
)
# In your app module:
# app.add_page(tasks_page, route="/tasks", on_load=TaskState.load_tasks)
Architecture Overview
Browser
โ
โ HTTP (/admin, /api, /static, ...)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ Django ASGI
โ โณ ORM ยท Admin ยท Sessions ยท Auth
โ
โ HTTP + WebSocket (Reflex SPA, /_event/...)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโบ Reflex ASGI
โ
โผ
Reflex event arrives
โ
โผ
DjangoEventBridge runs
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Reads session cookie โ
โ Loads Django session from DB โ
โ Resolves request.user โ
โ Binds synthetic HttpRequest โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Your @rx.event handler
(self.request.user is ready)
The three things the plugin does
- Plugin bootstrap โ Sets
DJANGO_SETTINGS_MODULEand callsdjango.setup()before any models are imported. - HTTP path dispatcher โ Routes matching path prefixes (
/admin,/api, etc.) to Django ASGI; everything else stays on Reflex. - Per-event bridge โ On every WebSocket event, rebuilds a synthetic
HttpRequest, loads the session, and resolvesrequest.user.
Commands
Use reflex django (or the standalone reflex-django) to run Django management commands with the same settings Reflex uses at runtime:
# Database migrations
uv run reflex django migrate
uv run reflex django makemigrations
# Admin user
uv run reflex django createsuperuser
# Interactive shell
uv run reflex django shell
# Static files
uv run reflex django collectstatic
# Any other management command
uv run reflex django <command> [options]
What's Next?
| Topic | Link |
|---|---|
| ๐ Full documentation | mohannadirshedat.github.io/reflex-django |
| โก Quickstart guide | docs/quickstart.md |
| ๐ Architecture deep-dive | docs/architecture.md |
| ๐ Session authentication | docs/authentication.md |
| ๐ Declarative CRUD (ModelState) | docs/reactive_model_state.md |
| ๐ ModelState vs ModelCRUDView | docs/model_state_and_crud_view.md |
| ๐ Deployment guide | docs/deployment.md |
| โ FAQ | docs/faq.md |
Author: Mohannad Irshedat ยท GitHub
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reflex_django-0.2.8.tar.gz.
File metadata
- Download URL: reflex_django-0.2.8.tar.gz
- Upload date:
- Size: 129.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.0 {"installer":{"name":"uv","version":"0.11.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80f99f737a2eb05022dad19f1de10bf68843604be0c1f2055e7c14c953d667c0
|
|
| MD5 |
3e0026f57f19c0a56a1eb19f6dc1bc30
|
|
| BLAKE2b-256 |
f4b534e95abe20b5066c2514869a62e61de7fbfa7e6654542c377249b4812a5d
|
File details
Details for the file reflex_django-0.2.8-py3-none-any.whl.
File metadata
- Download URL: reflex_django-0.2.8-py3-none-any.whl
- Upload date:
- Size: 125.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.0 {"installer":{"name":"uv","version":"0.11.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
47c6c3457bb1d4f1d28dcc0e29517be3dfefb3681de03744a3c1387d589a3db1
|
|
| MD5 |
3bd3ea032f5028dd358cc98580522679
|
|
| BLAKE2b-256 |
ebc4df391e42b8f6bda95c324b17dac85249fa262efd4b310baabbe59571a7a9
|