Skip to main content

Run a Django backend (Django ORM, Django Admin) alongside a Reflex app.

Project description

reflex-django

reflex-django

Run Django and Reflex in one process โ€” one command, zero glue.

PyPI Python Docs License

๐Ÿ“– 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

  1. Why reflex-django?
  2. Quick Install
  3. Django Settings Configuration
  4. Wire it into rxconfig.py
  5. Accessing the Logged-In User with AppState
  6. Simple CRUD Without Mixins
  7. Architecture Overview
  8. Commands
  9. 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 HttpRequest from WebSocket cookies and headers on every event.
  • Loads the Django session and resolves request.user automatically.
  • Exposes self.request directly 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 (or await 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

  1. Plugin bootstrap โ€” Sets DJANGO_SETTINGS_MODULE and calls django.setup() before any models are imported.
  2. HTTP path dispatcher โ€” Routes matching path prefixes (/admin, /api, etc.) to Django ASGI; everything else stays on Reflex.
  3. Per-event bridge โ€” On every WebSocket event, rebuilds a synthetic HttpRequest, loads the session, and resolves request.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

reflex_django-0.2.8.tar.gz (129.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

reflex_django-0.2.8-py3-none-any.whl (125.3 kB view details)

Uploaded Python 3

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

Hashes for reflex_django-0.2.8.tar.gz
Algorithm Hash digest
SHA256 80f99f737a2eb05022dad19f1de10bf68843604be0c1f2055e7c14c953d667c0
MD5 3e0026f57f19c0a56a1eb19f6dc1bc30
BLAKE2b-256 f4b534e95abe20b5066c2514869a62e61de7fbfa7e6654542c377249b4812a5d

See more details on using hashes here.

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

Hashes for reflex_django-0.2.8-py3-none-any.whl
Algorithm Hash digest
SHA256 47c6c3457bb1d4f1d28dcc0e29517be3dfefb3681de03744a3c1387d589a3db1
MD5 3bd3ea032f5028dd358cc98580522679
BLAKE2b-256 ebc4df391e42b8f6bda95c324b17dac85249fa262efd4b310baabbe59571a7a9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page