JOPS already runs in the browser, in hybrid mobile apps, inside Next.js and WordPress, and on the desktop with Electron. With jops-core 1.1.11, it comes to the Python world: a JOPS single-page app can now run in a Django project, either as its own Django app or embedded in any Django template with a new {% jops %} template tag.
Django stays the backend: your models, views, APIs, sessions and admin. JOPS adds class-based views with an Android-style lifecycle for the screens that behave like an application, with no Node.js build step in your deployment.
JOPS also has a home of its own now: the JOPS website, jops.ui-db.com, launched with 1.1.10, has guides for every platform, the core concepts, the full API reference and examples. The Django guide is already there.
Why JOPS and Django fit
Django developers tend to think in classes: models, class-based views, forms, admin. On the frontend, the usual choice is between server-rendered templates with some JavaScript sprinkled on, or a separate React or Vue project with its own toolchain, its own deployment and an API between the two.
JOPS offers a middle path. The SPA is a set of static files that Django serves like any other, from the same project, on the same origin. Views are classes, the lifecycle is explicit, and there's nothing to compile: collectstatic is the whole frontend build.
Two ways to use JOPS in Django
| SPA app | Embedded in a page | |
|---|---|---|
| What it is | A Django app holding a complete JOPS SPA, with its own page at /<app>/ | The SPA renders inside any Django template, next to the site's header, menu and content |
| How | npx jops-core init --django, or by hand | The {% jops %} template tag |
Both use the jops app: a small reusable Django app with the template tag and the jops-core library. It has no models and needs no migrations.
Install with npm or with pip
Not every Django team runs Node.js, so the jops app comes two ways:
- With npm:
npm i jops-core, thennpx jops-core init --djangoin the folder withmanage.py. It writes thejopsapp into the project, andnpx jops-core updaterefreshes it later. - Without npm:
pip install https://github.com/uidb-dev/jops-core/releases/latest/download/django-jops.tar.gz, then add"jops"toINSTALLED_APPS. The package is attached to every GitHub release, and a version-pinned URL works inrequirements.txt.
A SPA app in one command
npm i jops-core npx jops-core init --django # 1 Empty, 2 Hello World, 3 None; app name: jops_spa python manage.py runserver # open http://127.0.0.1:8000/jops_spa/ init creates the app with its static files, its page and one URL route, then adds the apps to INSTALLED_APPS and the route to urlpatterns. If your settings.py or urls.py doesn't have the standard startproject layout, it changes nothing there and prints the lines to add instead. It never overwrites an existing app.
jops/ the jops app ({% jops %} + library) jops_spa/ the SPA app static/jops_spa/src/ App.js, views, layouts, styles templates/jops_spa/index.html urls.py Embed a SPA in any Django page
The {% jops %} tag puts a JOPS app inside an existing template, between your site's own header and footer:
{% extends "base.html" %} {% load jops %} {% block content %} {% jops "jops_spa/src/App.js" base="jops_spa/" id="spa-root" class="spa" %} {% endblock %} The tag outputs the import map, the library in hosted mode, the app's root <div> and a small script that mounts the app. It also takes care of two things every Django developer will ask about:
- CSRF: the root
<div>carries the request's CSRF token asdata-csrf, ready for theX-CSRFTokenheader on requests that change data. - CSP: with django-csp, the inline scripts get the request's nonce, so a strict nonce policy keeps working.
const token = document.getElementById("spa-root").dataset.csrf; await fetch("/api/orders/", { method: "POST", headers: { "X-CSRFToken": token, "Content-Type": "application/json" }, body: JSON.stringify(order), }); New in the library: the app's base folder
Django serves static files from STATIC_URL, which might be /static/, /assets/ or a CDN domain, with hashed file names in production. Hard-coding /static/ into every layout path would break the first time someone changes that setting.
So 1.1.11 adds one opt-in option to the library: the app's base folder. Set it once, with data-base on the script tag or base in {% jops %}, and every relative path JOPS loads resolves against it:
// jops_spa/static/jops_spa/src/App.js root.loadLayout("src/layout/main.html"); // not "/static/jops_spa/src/..." The same app files then work under /static/, another prefix or a CDN, without a single change. Without a base, paths resolve exactly as before, so no existing app is affected. The option isn't Django-specific: it helps any app whose files are served from another prefix or domain.
What we tested it on
We first integrated JOPS into a Django test project by hand, then built the jops app and init --django from what we learned, and tested the result on the oldest and newest Django versions we support:
| Setup | Django / Python | Result |
|---|---|---|
runserver, DEBUG = True | 4.2 / 3.9 and 6.1 / 3.14 | Works, no code changes between versions |
gunicorn + nginx, collectstatic | 6.1 / 3.14 | Works, also with STATIC_URL = '/assets/' |
| gunicorn + WhiteNoise, hashed file names | 6.1 / 3.14 | Works, one library instance |
| Static files on another domain (CDN) with CORS | 6.1 / 3.14 | Works, every file from the CDN |
| django-csp nonce policy | 6.1 / 3.14 | Works, no CSP errors |
On each setup, the standalone page and the embedded page ran every route with no failed requests and no console errors. We also tested a CSRF write with and without the token, a second {% jops %} on one page, a wrong library URL, and both install paths, npm and pip.
Know before you start
- Django owns the path, JOPS owns the hash.
/jops_spa/is a Django route;/jops_spa/#details?id=7is a screen inside the app. One URL route per SPA is enough. - The SPA has no models. It fetches its data from your own views and APIs over
fetch(), same-origin, with the session cookie sent. - Scope the embedded app's CSS under its root class: its stylesheet applies to the whole Django page.
- One JOPS app per page with
{% jops %}. - Import the library only as
"jops-core", through the import map, so hashed file names never load a second copy. - A CDN
STATIC_URLneeds CORS headers on the static host.
Try it
- jops.ui-db.com, the new JOPS website: guides, concepts, API reference and examples
- The Django guide on the JOPS website
- jops-core on npm
- GitHub for releases, issues and discussions
- Introducing JOPS, if you're new to the framework
Six places to run, one way to write views
JOPS now runs in the browser, in hybrid iOS and Android apps, inside Next.js, WordPress and Django, and on the desktop with Electron. The views are the same classes everywhere; only the host changes. Tell us what you build with it, and which backend you'd like to see next.
JOPS is built and maintained at UIDB, a boutique team of senior engineers who build custom systems with our clients, side by side. If you're extending a Django system or planning one that needs to hold up for years, we'd be glad to talk.

