Ship your Django + djust app as a native iOS / Android app that runs entirely on the device — no backend server.
djust-mobile-toga runs your existing Django codebase as an on-device loopback
server inside a Toga +
Briefcase shell and points a native WebView
at it. The same code that serves your website serves the phone: offline, private
(the SQLite database stays in the app's sandbox), and reactive through djust's
LiveView-style server rendering over a local WebSocket. No REST API to build, no
servers to run.
When the app launches, BaseDjustApp starts a single-worker uvicorn bound to
127.0.0.1 serving your Django + djust ASGI app, then opens a native WebView
pointed at that loopback URL:
+-------------------------- phone ---------------------------+
| |
| +------------+ http://127.0.0.1:<port> |
| | WebView | ----- HTTP / WebSocket ------+ |
| | (your UI) | <---- HTML / WS frames ---+ | |
| +-----+------+ | v |
| | JS <-> Python bridges +----+-----------+ |
| v | uvicorn | |
| wallet / voice / AI / notifs | loopback,1 wkr | |
| +-------+--------+ |
| v |
| +----------------+ |
| | Django + djust | |
| | ASGI / SQLite | |
| +----------------+ |
| |
| No network. No remote backend. |
+------------------------------------------------------------+
It handles the parts that make "run Django on a phone" hard: the stdlib gaps
uvicorn + Django hit at import time on mobile (_multiprocessing, mimetypes),
a WebSocket implementation iOS can actually run (wsproto), a writable per-platform
data directory, off-the-UI-thread migrate + collectstatic, and status-bar
tinting. Optional native bridges let in-page JavaScript reach device capabilities.
The core shell — three modules:
| Module | Role |
|---|---|
app |
BaseDjustApp(toga.App) — data dir, Django prep, uvicorn boot, WebView, status bar. Your entry point. |
serve |
Loopback uvicorn helper (bypasses uvicorn.supervisors; single worker; ws=wsproto). |
shims |
The _multiprocessing / mimetypes compatibility patches uvicorn + Django need on iOS/Android. Install first. |
Plus optional, fail-soft native bridges — off their target platform (or when the native shim isn't compiled in) they report unavailable and no-op, so you can call them unconditionally and they never raise:
| Bridge | Capability | Platform |
|---|---|---|
bridge |
JavaScript ↔ Python calls (the mechanism the others build on) | iOS |
passkit |
Add-to-Apple-Wallet (.pkpass) + a {% wallet_add_button %} tag |
iOS |
voice |
On-device speech-to-text + text-to-speech | iOS |
apple_intelligence |
On-device Foundation Models Q&A | iOS 26+ |
notifications |
Local notifications | iOS · Android |
→ Native bridges guide · Platform matrix
pip install djust-mobile-toga
djustitself is not a hard dependency — install it separately. The mobile build needs platform-specific cross-compiled wheels that aren't on PyPI, so we don't make pip try to resolvedjustfrom an index it can't find.
Your app subclasses BaseDjustApp and points it at your ASGI app + settings:
# myapp/app.py
from djust_mobile_toga.app import BaseDjustApp
class MyApp(BaseDjustApp):
asgi_app_path = "myapp.asgi:application"
django_settings_module = "myapp.settings"
status_bar_color_argb = 0xFF14457E # opaque blue; omit for the system default
def on_app_ready(self) -> None:
# Optional post-launch hook (notifications, analytics, …).
pass
def main() -> MyApp:
return MyApp(formal_name="My App", app_id="org.example.myapp", app_name="myapp")In the Briefcase entry point, install the shims before anything imports uvicorn/Django, then launch:
# myapp/__main__.py
from djust_mobile_toga import shims
shims.install() # CALL FIRST
from myapp.app import main
main().main_loop()Your settings.py reads DJUST_MOBILE_DATA_DIR (set by BaseDjustApp at
startup) for DATABASES / MEDIA_ROOT, and otherwise it's an ordinary Django +
djust app.
This package is the Python runtime glue. The native build is yours:
-
Cross-compile djust for iOS (
aarch64-apple-ios,aarch64-apple-ios-sim) and/or Android (aarch64-linux-android,x86_64-linux-android). djust doesn't currently publish mobile wheels; the reference recipe ismaturin+ a BeeWare cross-platform venv for iOS, andcibuildwheel --platform androidfor Android. -
Repackage non-mobile native deps — e.g.
msgpack's pure-Python fallback needs re-tagging with iOS/Android platform tags so pip installs it under--platform(strip the.sos, emit…_iphoneos.whl/…_android_21_arm64_v8a.whl). -
Briefcase scaffold patches — on Android, the generated
pip-options.txtneeds an absolute--find-links wheelspath, andres/values/colors.xml'scolorPrimaryDarkshould matchstatus_bar_color_argb(the activity theme paints the bar before any runtime call lands). -
POST_NOTIFICATIONSpermission on Android 13+ if usingnotifications— declare it in your Briefcase[…android]block, or the system silently drops them. -
A settings module that respects
DJUST_MOBILE_DATA_DIR(see Quick start).
Per-feature Info.plist keys and the Apple Intelligence Swift shim are in the
native bridges guide.
Beta. Typed (py.typed), CI-gated (ruff + mypy + pytest on Python 3.11–3.13),
and tested — the core (serve / shims / apps / templatetags / bridge) is
at ~100% off-device coverage; the iOS/Android native bodies are hand-verified on
a device. The public API is stabilizing but may still evolve before 1.0 — pin a
version.
- Getting started — consuming-app shape, Django settings, building with Briefcase.
- Native bridges — speech / wallet / Apple Intelligence / notifications, with
Info.plistkeys and Swift shims. - Platform support — capability matrix + the fail-soft contract.
- Contributing · Releasing
MIT — see LICENSE.