Django-React Integration
Smarter’s React integration is a bridge between Django’s server-rendered web platform and modern React user interfaces. Rather than treating React as a disconnected single-page application bolted onto Django, Smarter implements React as a first-class extension of the Django runtime itself. Django keeps what it does well, which is authentication, session management, template rendering, routing and server-side orchestration. React gets what it does well, which is rich, highly interactive user interfaces. The approach has scaled. Smarter’s web console now hosts more than 30 independent React applications in a single Django deployment, each with its own runtime configuration, API endpoints and authentication context.
A central design goal is to separate frontend component development from backend deployment concerns. React developers build components with standard modern tooling, while Django takes responsibility for asset discovery, manifest analysis, dependency resolution, template injection and configuration propagation. This keeps integration code out of React components, and gives every React app in the platform the same deployment architecture.
At the core of this approach is an asset pipeline built around the Vite.js manifest.json build artifact. Smarter’s custom Django template tags read the manifest, validate it, and recursively walk it to discover every JavaScript and CSS file the app needs (hashed assets, shared chunks, vendor bundles and code-split modules), then inject the correct <script> and <link> elements into the Django template. Because the integration is manifest-driven, React builds are cache-safe and environment-agnostic, with no hardcoded filenames and no manual template maintenance.
Smarter’s Vite configuration aligns React’s development and production environments with Django’s runtime behavior. Builds are emitted directly into Django’s static file tree, the Vite development server proxies API traffic to Django, and production builds strip debugging statements. Developers keep hot module replacement, and the apps keep full compatibility with Django’s authentication, CSRF and other middleware protections.
The boundary between Django and React is deliberately thin. A Django template provides a DOM mounting point for the React app and serializes server-side context into custom HTML attributes on it. When React starts, it reads those attributes into a session context, so API endpoints, cookie names, a request id and feature flags flow from Django into the frontend without a bootstrap API call or inline JavaScript.
The Django view stays thin too. It handles authentication, routing, template selection and the runtime configuration. Everything else lives in reusable template tags and Vite build conventions.

The Terminal Application as an Example
Let’s tie the pieces together with a concrete example that shows manifest analysis, Vite configuration, custom Django template tags, the template’s mounting point, and server-side context propagation, and how they work together to bring a React app into the Smarter web console.
The web console’s Terminal page is a good example. It is a highly interactive React component, drawn with xterm.js, that streams data in both directions and depends on Django’s authentication and server-side context. It runs a real bash shell inside the server’s container, so an instructor can show a class the running platform from the inside, with its files, its processes, its environment, manage.py shell. Because that shell has the server’s credentials, the Terminal is for superusers only, and is off unless SMARTER_ENABLE_WEB_CONSOLE_TERMINAL is true. The React app POSTs to a ticket API for a single-use ticket, then opens the terminal’s WebSocket with it.

React Build Manifest
The React build’s manifest.json is generated by Vite and is the single source of truth for the names and locations of every JavaScript and CSS asset in the build. Its contents vary widely from app to app, depending on how many third-party modules and assets the app bundles. The asset filenames carry content hashes for cache busting, so they can’t be known in advance.
Following is the manifest.json of the Terminal Application.
{
"_rolldown-runtime-B8sk0Y4v.js": {
"file": "assets/rolldown-runtime-B8sk0Y4v.js",
"name": "rolldown-runtime"
},
"_xterm-BVTBumqj.js": {
"file": "assets/xterm-BVTBumqj.js",
"name": "xterm",
"imports": ["_rolldown-runtime-B8sk0Y4v.js"],
"css": ["assets/xterm-kHJ-D0s7.css"]
},
"_xterm-kHJ-D0s7.css": {
"file": "assets/xterm-kHJ-D0s7.css",
"src": "_xterm-kHJ-D0s7.css"
},
"index.html": {
"file": "assets/index-B1eOzN5c.js",
"name": "index",
"src": "index.html",
"isEntry": true,
"imports": ["_rolldown-runtime-B8sk0Y4v.js", "_xterm-BVTBumqj.js"],
"css": ["assets/index-58MXwt-L.css"]
},
"_custom": {
"buildTime": "2026-10-12T01:09:47.988Z",
"version": "0.3.0",
"buildEnv": "production"
}
}
The entry whose "isEntry" is true is the app’s entry point, and the root of the dependency graph. Django walks the graph from there, through every import, to collect the complete set of files to add to the template. _custom isn’t a Vite chunk. It is build information that Smarter’s Vite configuration adds after the build (see below).

Django doesn’t read the manifest as loose JSON. It validates it against a Pydantic model, ViteManifest. Chunks accept fields that newer versions of Vite or Rolldown add, so a Vite upgrade doesn’t break page rendering, while _custom accepts only its three fields. A manifest that is missing, isn’t JSON, or doesn’t match the model is logged as an error and treated as absent, and the page renders without the React app.
Vite Configuration
The manifest is generated by Vite, and its structure and contents are determined entirely by the Vite configuration. Smarter’s Vite configuration is the integration layer between the React frontend and the Django backend. Rather than embedding deployment logic in React applications, Smarter keeps it in the build pipeline, so React components can be developed independently of how their assets are hosted, versioned, discovered and injected into Django templates.
The configuration takes care of the following.
- Manifest Generation writes a
manifest.jsonwith the hashed filenames of every JavaScript, CSS and chunk file in the build. - Build Information comes from the
addCustomManifestDataplugin, which adds the_customentry, with the build time, the package’s version and the build’sNODE_ENV. - Development Proxying means that requests from the Vite development server for APIs and platform-wide static files are forwarded to the Django development server, so a React app can run locally against a live Django backend.
- Static Asset Integration means builds go directly into Django’s static file tree, so they take part in
collectstaticand are served alongside the rest of the platform. - Security Compatibility comes from serving the apps from the same origin as Django, which keeps Django’s authentication, CSRF protection, session cookies and same-origin security model intact.
- Production Hardening removes
console.debug()calls from production bundles, to cut console noise and avoid leaking runtime information. Vite 8 minifies with the Oxc minifier, so the configuration marksconsole.debugas a pure function and dead-code elimination removes it. - Caching Optimization puts large third-party dependencies such as xterm.js into their own bundles, so the browser keeps them cached while application code changes.
- Developer Experience gets hot module replacement and fast incremental rebuilds, without breaking compatibility with the Django runtime.
The following is the Vite configuration of the Terminal Application (vite.config.ts), slightly abridged.
const packageName = packageJson.name; // "@smarter/terminal-emulator"
// Adds build information to manifest.json after each build.
const addCustomManifestData: PluginOption = {
name: "add-custom-manifest-data",
writeBundle() {
const manifestPath = path.resolve(
import.meta.dirname,
`../../../smarter/static/react/${packageName}/manifest.json`,
);
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
manifest._custom = {
buildTime: new Date().toISOString(),
version: packageJson.version,
buildEnv: process.env.NODE_ENV || "development",
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
}
},
};
export default defineConfig(({ command }: ConfigEnv) => ({
plugins: [react(), addCustomManifestData],
// Vite's dev server serves from '/'. Builds are served by Django from /static/.
base: command === "serve" ? "/" : `/static/react/${packageName}/`,
resolve: {
alias: {
"@": path.resolve(import.meta.dirname, "./src"),
},
},
build: {
// Read by the Django template tags, to find the hashed file names.
manifest: "manifest.json",
// Django's static directory, so that collectstatic picks up the build.
outDir: `../../../smarter/static/react/${packageName}`,
emptyOutDir: true,
rolldownOptions: {
output: {
entryFileNames: "assets/[name]-[hash].js",
chunkFileNames: "assets/[name]-[hash].js",
assetFileNames: "assets/[name]-[hash][extname]",
// xterm.js gets its own bundle, so that it stays cached in the browser.
manualChunks(id: string) {
if (id.includes("node_modules/xterm") || id.includes("node_modules/@xterm")) {
return "xterm";
}
return undefined;
},
// Strip console.debug() from production builds.
minify: {
compress: {
treeshake: {
manualPureFunctions: ["console.debug"],
},
},
},
},
},
},
// In development, send API and platform static file requests to Django.
server: {
proxy: {
"/api": "http://localhost:9357",
"/assets": {
target: "http://localhost:9357",
changeOrigin: true,
rewrite: (path: string) => `/static${path}`,
},
"/common-styles.css": {
target: "http://localhost:9357",
changeOrigin: true,
rewrite: (path: string) => `/static${path}`,
},
"/dashboard/": "http://localhost:9357",
[`/static/react/${packageName}/`]: {
target: "http://localhost:5173",
changeOrigin: true,
rewrite: (path: string) => path.replace(new RegExp(`^/static/react/${packageName}/`), "/"),
},
"/static": {
target: "http://localhost:9357",
changeOrigin: true,
},
"/workbench/": "http://localhost:9357",
},
},
}));
Django Template Tags
Django template tags are a server-side extension mechanism that lets a template call Python code with a declarative syntax such as {% tag_name %}. They are a natural fit for a Vite manifest, because the manifest is a machine-readable map from source entry points to their hashed production assets. Each React app has a small template tag module, which creates one SmarterReactTemplateTagManager for the app’s package.
from django import template
from smarter.lib.django.templatetags.smarter_react_contract import ReactAppAssets
from smarter.lib.django.templatetags.smarter_react_templatetag_manager import (
SmarterReactTemplateTagManager,
)
register = template.Library()
templatetag_manager = SmarterReactTemplateTagManager(
app_name="@smarter/terminal-emulator", templatetag_name=__name__
)
@register.simple_tag
def terminal_emulator_react_assets() -> ReactAppAssets:
"""The CSS and JS files of the terminal emulator's entry point."""
return templatetag_manager.reactapp_build_assets
The manager loads static/react/@smarter/terminal-emulator/manifest.json from Django’s collected static files, validates it, finds the entry point, and walks its imports depth first. It returns a ReactAppAssets model with js and css lists, the entry point’s own files first. The result is cached for the life of the server process, so the manifest is read once rather than on every request. The flip side is that if a page renders an empty root element and the browser console shows nothing, the app’s manifest.json was missing when the server started. Build and collect the static files, then restart the server.
The template uses the tag as an iterator over the asset filenames, without knowing anything about the manifest.
{% block style_extra %}
{{ block.super }}
{% terminal_emulator_react_assets as assets %}
{% for css_file in assets.css %}
<link class="smarter" rel="stylesheet" href="{% static 'react/@smarter/terminal-emulator/' %}{{ css_file }}">
{% endfor %}
{% endblock %}
{% block javascript_extra %}
{{ block.super }}
{% terminal_emulator_react_assets as assets %}
{% for js_file in assets.js %}
<script class="smarter" type="module" src="{% static 'react/@smarter/terminal-emulator/' %}{{ js_file }}"></script>
{% endfor %}
{% endblock %}
See SmarterReactTemplateTagManager, which also documents the Pydantic models of the manifest and of the assets.
Django Template
The Django template provides a DOM element for the React app to mount to, and maps the Django view’s context onto custom HTML attributes of that element. The attributes are the transport layer for the app’s configuration. Server-side context passes into the client-side React app when it starts, with no inline JavaScript and no extra API call during the initial page load.
The context typically includes API endpoint URLs, the names of Django’s session and CSRF cookies, the cookie domain, feature flags, and anything else the React app needs at runtime. The Terminal’s template renders this.
<div
id="smarter-terminal-emulator-root"
smarter-csrf-cookie-name="csrftoken"
smarter-django-session-cookie-name="sessionid"
smarter-cookie-domain="alpha.platform.example.com"
smarter-api-path="/dashboard/terminal/api/ticket/"
react-debug-mode="false"
smarter-request-id="..."
>
</div>
The React Side
When the React app starts, its main.tsx finds the mounting element, reads the attributes with Element.getAttribute(), stops with an error if a required one is missing, and builds a SessionContext that it passes to the root component as a prop.
const rootEl = document.getElementById("smarter-terminal-emulator-root");
if (!rootEl) throw new Error("Root element not found");
const ApiUrl = rootEl.getAttribute("smarter-api-path");
const csrfCookieName = rootEl.getAttribute("smarter-csrf-cookie-name");
const djangoSessionCookieName = rootEl.getAttribute("smarter-django-session-cookie-name");
const cookieDomain = rootEl.getAttribute("smarter-cookie-domain") || window.location.hostname;
const debugMode = rootEl.getAttribute("react-debug-mode")?.toLowerCase() === "true";
const smarterRequestId = rootEl.getAttribute("smarter-request-id") || "";
if (!ApiUrl) throw new Error("API URL not found in root element attributes");
// ... the same check for the other required attributes
const sessionContext: SessionContext = {
ApiUrl,
csrfCookieName,
djangoSessionCookieName,
cookieDomain,
debugMode,
smarterClient: projectName,
smarterClientVersion: projectVersion,
smarterRequestId,
smarterCapabilities: ["terminal"],
};
createRoot(rootEl).render(<App sessionContext={sessionContext} />);
SessionContext and the code that calls Django live in @smarter/common, a package that every React app in the workspace shares. Its fetchDjangoUrl() sends requests with the session cookie, an X-CSRFToken header read from the CSRF cookie, and a set of X-Smarter-* headers that identify the client, its version, the request id and its capabilities. Any new custom header must also be added to Django’s CORS_ALLOW_HEADERS.
From React’s point of view, the boundary stays simple. The component receives its configuration and endpoints as props, and never references a static asset filename or knows how its assets were built and injected.
Django View
The Django view maps the URL to the template and generates the context the template renders. It stays thin, handling only authentication, template selection, and the runtime configuration the React app needs. Endpoint URLs come from Django’s reverse() and URL namespaces rather than hardcoded strings, so routing stays defined in one place.
This is the Terminal’s view, slightly abridged. Its URL patterns wrap it in require_terminal(), which returns a 404 while the Terminal is turned off, and to every signed-in user who isn’t a superuser.
class TerminalEmulatorView(SmarterAuthenticatedNeverCachedWebView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
context = {
"terminal": {
"root_id": "smarter-terminal-emulator-root",
"csrf_cookie_name": settings.CSRF_COOKIE_NAME,
"django_session_cookie_name": settings.SESSION_COOKIE_NAME,
"cookie_domain": settings.SESSION_COOKIE_DOMAIN,
# the ticket api, whose single-use ticket opens the terminal's WebSocket.
"api_url": reverse(
DashboardReverseNames.namespace,
DashboardTerminalReverseNames.namespace,
DashboardTerminalReverseNames.ticket,
),
"react_debug_mode": switch_is_active(SmarterWaffleSwitches.ENABLE_REACTAPP_DEBUG_MODE),
"smarter_request_id": self.generate_smarter_request_id(),
}
}
self.template_path = "react/terminal-emulator.html"
return render(request, self.template_path, context=context)
Build, Deploy and CI/CD Considerations
The React apps are packages of one npm workspace, smarter/react/packages, one per console page, plus shared packages such as @smarter/common. Each app’s build, with its own manifest.json, goes into its own folder of Django’s static files, such as static/react/@smarter/terminal-emulator/.
From Django’s point of view, compiled React assets are no different from any other static file, such as CSS, images or JavaScript. That is intentional and central to the design. Because the builds land in Django’s static file tree before collectstatic runs, the React build pipeline stays decoupled from the Django runtime. React apps can be developed, versioned, tested and rebuilt independently while still flowing through Django’s deployment pipeline, and Django stays responsible for serving the static files, cache management and template rendering.
Some operational details are worth keeping in mind.
- Source control exclusion means compiled React builds are never committed to git. They are build artifacts, regenerated by every build.
- Build prerequisites matter, because the templates and template tags depend on
manifest.jsonand its assets, so the React build must succeed at least once before Django can render a React page. Keep this in mind when, for example, you are tinkering with versions inpackage.json. - CI/CD pipelines start from scratch. A GitHub Actions workflow starts from a clean checkout without compiled frontend assets, so the React build runs early, before the Docker build,
collectstatic, integration tests, or deployment. - Static file synchronization needs care. In local development, keep in mind the difference between Vite’s development server, the build output in Django’s static directory, and Django’s collected
staticfilesdirectory. Stale builds in one of them can lead to confusing behavior. - Container builds come last. A Docker image that serves React pages must be built after the React build, so that the bundles and their manifests are inside the image.
- Published packages include Smarter Chat, the React chat of the LLMClient prompt engineering workbench, is also published to npm as
@smarter.sh/ui-chat, and so is@smarter.sh/ui-widgets. Both are ordinary packages of the workspace, built, tested and served like every other, and published with each Smarter release. - Convenience tooling covers the common builds.
make react-buildbuilds every React app, collects the static files and builds the Docker image.make react-build-ciis the build that CI runs.
Together, these conventions give a predictable, reproducible deployment model that works the same way in local development, CI/CD workflows, Docker builds and production. The React integration keeps the operational simplicity of a Django deployment while allowing modern frontend build pipelines and React development workflows.
For the full technical reference, including a page for each React app, see Django-React Integration in the Smarter documentation.
