# Pluggable Backends

> Run this documentation site on Flask, FastAPI or Quart with one environment variable, and learn what each backend unlocks.

**Site index:** [https://boilerplate.2plot.dev/llms.txt](https://boilerplate.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 8 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://boilerplate.2plot.dev/sitemap.xml  


---

.. llms_copy::Pluggable Backends

.. toc::

### Overview

Dash **4.1+** introduces a pluggable server backend. Instead of being hard-wired to Flask, a Dash app can now be created on top of **FastAPI** or **Quart**, unlocking native async, websocket callbacks, and a transport surface that works well with the new MCP tooling shipping in Dash 4.3.

This boilerplate exposes the choice through a single environment variable so you can flip backends without editing code:

```bash
# Default — fully featured, all integrations on
DASH_BACKEND=flask python run.py

# Async backends (install the extra first)
pip install "dash[fastapi]"
DASH_BACKEND=fastapi python run.py

pip install "dash[quart]"
DASH_BACKEND=quart python run.py
```

The badge in the header and the navbar reflects whichever backend booted, so you always know what's running.

---

### How it's wired

The active backend is resolved once in `lib/backend.py` and threaded into both `run.py` and the UI components:


```python
# File: lib/backend.py

"""
Backend selection helper for the Dash documentation boilerplate.

Dash 4.1+ supports pluggable backends:
    app = Dash(backend="flask"  | "fastapi" | "quart")

This module owns the single source of truth for which backend the app is
running on, so both `run.py` and UI components (e.g. the navbar badge) stay
in sync.

Backend is selected via the ``DASH_BACKEND`` environment variable. Falls back
to ``flask`` if unset or invalid.
"""
from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Literal

try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    pass

BackendName = Literal["flask", "fastapi", "quart"]

SUPPORTED_BACKENDS: tuple[BackendName, ...] = ("flask", "fastapi", "quart")
DEFAULT_BACKEND: BackendName = "flask"


@dataclass(frozen=True)
class BackendInfo:
    name: BackendName
    label: str
    color: str          # DMC color name
    icon: str           # iconify icon id
    description: str
    is_async: bool


_BACKEND_INFO: dict[str, BackendInfo] = {
    "flask": BackendInfo(
        name="flask",
        label="Flask",
        color="gray",
        icon="simple-icons:flask",
        description="WSGI backend. Default. Full feature parity with Dash 3.x.",
        is_async=False,
    ),
    "fastapi": BackendInfo(
        name="fastapi",
        label="FastAPI",
        color="teal",
        icon="simple-icons:fastapi",
        description="ASGI backend. Adds websocket callbacks, async support and MCP-friendly transport.",
        is_async=True,
    ),
    "quart": BackendInfo(
        name="quart",
        label="Quart",
        color="violet",
        icon="simple-icons:python",
        description="ASGI backend with a Flask-compatible API surface.",
        is_async=True,
    ),
}


def resolve_backend(name: str | None = None) -> BackendName:
    """Resolve the backend to use.

    Reads ``DASH_BACKEND`` env var when ``name`` is not given. Unknown values
    fall back to the default backend rather than raising — the docs site
    should always boot.
    """
    raw = (name or os.environ.get("DASH_BACKEND") or DEFAULT_BACKEND).strip().lower()
    if raw not in SUPPORTED_BACKENDS:
        return DEFAULT_BACKEND
    return raw  # type: ignore[return-value]


def get_backend_info(name: str | None = None) -> BackendInfo:
    return _BACKEND_INFO[resolve_backend(name)]
```


`run.py` then passes the resolved name straight to the `Dash()` constructor:

```python
from lib.backend import resolve_backend, get_backend_info

BACKEND = resolve_backend()
BACKEND_INFO = get_backend_info(BACKEND)
IS_FLASK = BACKEND == "flask"

app = Dash(
    __name__,
    backend=BACKEND,            # <-- new in Dash 4.1
    suppress_callback_exceptions=True,
    use_pages=True,
    ...
)
app._backend_info = BACKEND_INFO
```

The only remaining Flask-only integration is the `before_request` analytics hook, which the boilerplate transparently swaps for a Starlette `BaseHTTPMiddleware` when the backend is FastAPI. `add_llms_routes(app)` from `dash-improve-my-llms` 2.0 auto-detects the backend and mounts its own router under all three — no `IS_FLASK` gate is needed for the AI/LLM surfaces anymore.

---

### What you get on each backend

| Feature | Flask | FastAPI | Quart |
|--|--|--|--|
| Synchronous callbacks | yes | yes | yes |
| `async def` callbacks | n/a | yes | yes |
| Websocket callbacks (`ctx.websocket`) | no | yes (Dash 4.2+) | yes (Dash 4.2+) |
| Persistent callbacks (Dash 4.2 rc) | no | yes | yes |
| MCP server / `mcp_enabled` decorator (Dash 4.3) | partial | yes | yes |
| `add_llms_routes` (dash-improve-my-llms 2.0) | yes | yes | yes |
| `/llms.txt`, `/robots.txt`, `/sitemap.xml` | yes | yes | yes |
| `gunicorn` deploy | yes | n/a | n/a |
| `uvicorn` deploy | n/a | yes | yes |

The AI/LLM surfaces (`/llms.txt`, `/robots.txt`, `/sitemap.xml`, bot middleware, MCP bridge) work identically under all three backends — pick the one that matches the rest of your stack.

---

### Choosing a backend

#### Flask (default)

The safe choice. Everything in this boilerplate works without changes, including the analytics tracker, the `dash-improve-my-llms` integration, and the SEO routes.

Deploy with `gunicorn run:server -b 0.0.0.0:8550` (already wired into the included `Dockerfile`).

#### FastAPI

Pick this when you want:

- Async callbacks (`async def update_table(...)`)
- Real-time websocket callbacks for streaming UI updates
- A first-class MCP server using Dash 4.3's framework utilities
- Easy interop with an existing FastAPI app — just pass it in:

```python
from fastapi import FastAPI
from dash import Dash

api = FastAPI()
api.add_api_route("/healthz", lambda: {"ok": True})

app = Dash(__name__, server=api)        # Dash detects FastAPI automatically
```

Deploy with `uvicorn run:server --host 0.0.0.0 --port 8550`.

#### Quart

A drop-in option for teams already on Quart, or who want async with Flask-style ergonomics (`@server.before_request` becomes `@server.before_request` returning a coroutine).

```python
from quart import Quart
from dash import Dash

server = Quart(__name__)
app = Dash(__name__, server=server)
```

Deploy with `uvicorn run:server --host 0.0.0.0 --port 8550`.

---

### Custom backends

`Dash(backend=...)` also accepts a subclass of `dash.backends.base_server.BaseDashServer` plus matching request/response adapters, so internal frameworks (a hardened ASGI server, an in-process test harness, etc.) can be plugged in without forking Dash. See the `dash.backends` module for the protocol.

---

### Migration checklist for your own apps

1. Bump `dash>=4.1.0` (and `dash-mantine-components>=2.7.0` if you use DMC).
2. Replace any `app.run_server(...)` calls with `app.run(...)`.
3. Audit every `@app.server.before_request` / `@app.server.route` — Flask-only. Wrap them in an `if backend == "flask":` guard or rewrite as FastAPI/Quart middleware.
4. Install the extra: `pip install "dash[fastapi]"` or `pip install "dash[quart]"`.
5. Pass `backend="fastapi"` (or `"quart"`) to `Dash(...)`, or set `DASH_BACKEND` if you're using this boilerplate's helper.
6. Swap your process manager: `gunicorn` → `uvicorn` (or `hypercorn`).

---

### What this site does today

This page is itself rendered by whichever backend `DASH_BACKEND` points at. The header badge tells you which one — change the env var, restart `run.py`, and the badge updates to match.


---

*Source: /backends*
