Django Integration

bext fronts Django as a reverse-proxy edge. Django keeps every bit of its own stack — ORM, admin, forms, middleware, management commands — and bext handles TLS, HTTP/2, HTTP/3, the WAF, caching, and static file delivery. The upstream is a regular Gunicorn or Uvicorn process listening on 127.0.0.1; bext never runs Python in-process.

Architecture

client ──TLS──▶ bext edge ──HTTP/1.1──▶ Gunicorn / Uvicorn ──▶ Django
                │                              │
                │  serves /static/**           │
                │  serves /media/**            │
                │  WAF, cache, tracing         │
                │  HTTP/2, HTTP/3              │
                └── upstream health checks ────┘

Everything after the upstream boundary is stock Django. Everything before it is stock bext. The two meet over localhost HTTP/1.1.

Scaffold

bext new my-app --template @bext/starter-django
cd my-app
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput

Gunicorn (sync WSGI)

Gunicorn is the default for classic Django deployments: sync views, ORM queries, templated responses. Pick it if you're not writing async def view functions.

gunicorn myproject.wsgi \
  --bind 127.0.0.1:8000 \
  --workers 4 \
  --threads 2 \
  --timeout 30

Rule of thumb: workers = 2 * cores + 1 for IO-bound Django work; bext absorbs cold-start jitter and queues bursts at the edge, so you usually don't need as many workers as you would behind nginx.

Uvicorn (async ASGI)

Uvicorn is the right choice when any view is async def, when you stream responses, or when you want to use Django Channels for websockets. It runs on uvloop and handles ASGI lifespan events cleanly.

uvicorn myproject.asgi:application \
  --host 127.0.0.1 \
  --port 8000 \
  --workers 4

bext doesn't care which one you pick — it's a reverse proxy destination. Switching is systemctl stop django-gunicorn then systemctl start django-uvicorn; no bext config change needed.

Static files served by bext

Django's staticfiles view is fine in development but should never run in production. Bext serves static content directly from STATIC_ROOT with content-addressed URLs, pre-compressed brotli / gzip variants, and cache-control: immutable — an order of magnitude less latency than asking Django for a PNG.

In bext.config.toml:

[[route_rules]]
pattern = "/static/**"
render = "static"
headers = { "cache-control" = "public, max-age=31536000, immutable" }

[[route_rules]]
pattern = "/media/**"
render = "static"

And in myproject/settings.py:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

After deploying new assets run python manage.py collectstatic; bext picks up the new files without a reload because static routes are filesystem-backed.

bext.config.toml

[server]
port = 3000
app_dir = "."

[framework]
type = "proxy"

[upstreams.django]
keepalive = 32
connect_timeout_ms = 2000
read_timeout_ms = 30000

[[upstreams.django.servers]]
url = "http://127.0.0.1:8000"

[upstreams.django.health]
enabled = true
path = "/"

[[route_rules]]
pattern = "/static/**"
render = "static"

[[route_rules]]
pattern = "/media/**"
render = "static"

[[route_rules]]
pattern = "/**"
proxy = "django"

[cache]
enabled = true

[waf]
enabled = true

The starter ships with this file pre-wired. Point STATIC_ROOT at ./staticfiles in settings so the directory layout matches.

systemd units

Gunicorn

# /etc/systemd/system/django-gunicorn.service
[Unit]
Description=Gunicorn for my Django app
After=network.target

[Service]
Type=notify
User=django
Group=django
WorkingDirectory=/srv/my-app
Environment="PATH=/srv/my-app/.venv/bin"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings"
ExecStart=/srv/my-app/.venv/bin/gunicorn \
          --bind 127.0.0.1:8000 \
          --workers 4 \
          --threads 2 \
          --timeout 30 \
          myproject.wsgi
Restart=always

[Install]
WantedBy=multi-user.target

Uvicorn

# /etc/systemd/system/django-uvicorn.service
[Unit]
Description=Uvicorn for my Django app (ASGI)
After=network.target

[Service]
Type=simple
User=django
Group=django
WorkingDirectory=/srv/my-app
Environment="PATH=/srv/my-app/.venv/bin"
Environment="DJANGO_SETTINGS_MODULE=myproject.settings"
ExecStart=/srv/my-app/.venv/bin/uvicorn \
          --host 127.0.0.1 \
          --port 8000 \
          --workers 4 \
          myproject.asgi:application
Restart=always

[Install]
WantedBy=multi-user.target

Pair either unit with bext's own systemd unit. systemctl reload bext re-reads bext.config.toml without dropping connections — Django deploys are independent of bext deploys.

Trust the X-Forwarded-Proto header

bext sets X-Forwarded-Proto and X-Forwarded-For on every proxied request. Django needs a one-line setting to honour them:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True

With that in place, request.is_secure() correctly returns True when the client came in over TLS even though the upstream leg is plain HTTP.

Caching layer

Pair bext's fragment cache with Django's @vary_on_headers / @cache_control decorators. Django sets Cache-Control and bext honours it automatically; there's no bext-specific annotation.

For tag-based invalidation from inside a Django view, POST to the bext admin endpoint:



requests.post(
    "http://127.0.0.1:3000/__bext/cache/invalidate",
    json={"tags": ["products"]},
)

Database connections and health checks

bext's upstream health checks ping / by default. Point them at a lightweight view (not /health unless you define it, to avoid a 404) or add a dedicated health endpoint in urls.py that returns a small JSON body without touching the DB.

Catalog detection

Projects are detected automatically by bext site autoconfigure when all three of the following hold:

- manage.py exists at the project root

- requirements.txt or pyproject.toml lists django

- no other framework rule fires first

The detection rule lives in crates/bext-import/catalog/frameworks.yaml under django.

Known gaps

  • Async-only cache invalidation: Django's async cache backends are still evolving; bext cache invalidation from Channels consumers uses a thread-pool fallback. - Channels websockets: route LiveView-style WS traffic through a dedicated upstream (or a second bext upstream group) rather than the same one that serves request-response traffic, to avoid starving worker slots during long-held connections. - Multi-site deployments: for a single bext process fronting multiple Django apps, give each app its own upstream group and its own hostname-bound route rules.