Architecture Notes¶
Status: intended implementation shape, with the runnable development slice, staged bundled-runtime contract, sign/notarization-aware Electron packaging workflow, minimal Electron connected updater path, and experimental Tauri plus Positron shells now in place
Runtime Model¶
The expected runtime is intentionally simple:
flowchart TD
A[Electron main process]
B[Launcher script]
C[Django app on 127.0.0.1:<random-port>]
D[Task runner process]
E[SQLite in writable app-data path]
F[Health endpoint returns 200]
G[Electron BrowserWindow loads local URL]
A -->|starts bundled Python runtime| B
B -->|starts Django| C
B -->|starts db_worker| D
C --> E
D --> E
C --> F
F -->|readiness confirmed| G
Key expectations:
Electron owns process startup and shutdown
Django and the task worker are treated as local backend processes, not as remote services
Electron binds Django to
127.0.0.1on a random port before opening the rendererthe renderer loads normal Django pages over localhost
Electron now adds a per-session shell-to-Django auth token on top of the localhost bind: the main process passes
DESKTOP_DJANGO_AUTH_TOKENto Django and injectsX-Desktop-Django-Tokenonly for the exact local Django originTauri and Positron use the same Django token setting but acquire an HttpOnly same-origin cookie by first loading
/desktop-auth/bootstrap/?token=...&next=/; Django validates the token, sets the cookie, and redirects to the app URL without the tokenthe shell token is a channel check for localhost requests, not a CSRF replacement and not a value exposed through preload, a shell bridge, or normal page JavaScript
Tauri and Positron use the bootstrap cookie flow because their current web view paths do not have an Electron-equivalent external-localhost per-request header injection path
SQLite lives in a writable per-user app-data directory and stores both app data and task queue rows
Current shell split note:
Electron remains the baseline shell and the most complete release-oriented packaging lane
Tauri now also has an experimental GitHub-hosted artifact lane, while keeping the same staged-backend subprocess model
Tauri keeps the same staged-backend subprocess model locally
Positron keeps shell-local ownership of an in-process Django server plus an in-process worker thread
Current Repo Shape¶
This structure stays intentionally small, but it now includes the extra shell, docs, and wrapping support files that make the current starter usable as both a teaching repo and an adaptation workflow:
desktop-django-starter/
├── README.md
├── assets/
│ └── brand/
├── cli/
│ ├── src/dds/
│ └── tests/
├── docs/
│ ├── shells/
│ ├── architecture.md
│ ├── release.md
│ ├── design-guide.md
│ └── specification.md
├── scripts/
│ ├── bundled-python.cjs
│ └── stage-backend.cjs
├── shells/
│ ├── electron/
│ │ ├── assets/icons/
│ │ ├── scripts/
│ │ ├── main.js
│ │ ├── preload.cjs
│ │ └── package.json
│ ├── tauri/
│ │ ├── package.json
│ │ ├── scripts/
│ │ ├── src/
│ │ └── src-tauri/
│ └── positron/
│ ├── pyproject.toml
│ ├── scripts/
│ ├── resources/
│ └── src/
├── skills/
│ └── wrap-existing-django-in-electron/
├── manage.py
├── pyproject.toml
├── src/
│ ├── desktop_django_starter/ # Django project package
│ ├── example_app/
│ └── tasks_demo/
└── tests/
Notes:
script names are illustrative; the startup and packaging behavior matters more than exact filenames
staged packaged-backend helper scripts now live under
scripts/src/is preferred over a flatter package layout because it maps cleanly to packaging and later app replacementsrc/layout means the launcher and packaging scripts must set import paths deliberately rather than relying on the current working directorythe starter should keep Electron and Django code visibly separate
Startup Contract¶
The current implementation follows this sequence:
Electron creates a local splash window immediately after
app.whenReady().Electron chooses an open localhost port.
Electron runs migrations using either the bundled runtime or a local development interpreter.
Electron starts Django.
Django exposes a dedicated health endpoint.
Electron polls that endpoint until it succeeds or times out.
Electron starts the single
db_workerprocess only after Django is healthy.Electron loads the main app window only after both backend processes have started cleanly, then closes the splash window once the main window is ready to show.
Closing the desktop app shuts down both child processes; on Windows, the current Electron implementation does this with explicit forced process-tree termination rather than a graceful drain.
Single-instance expectation:
packaged mode should behave as a single-instance desktop app
Electron and Tauri currently focus the existing window instead of starting a second backend bootstrap path
Positron now enforces the same single-instance boundary with an app-data lock file, but currently exits the second launch instead of focusing the existing window
this avoids concurrent startup work against the same per-user SQLite database, including migration races during app launch
Health endpoint expectation:
use a dedicated route such as
/health/return HTTP 200 once Django startup is complete
keep the response simple and stable
Runtime Modes¶
The implementation should document the distinction between:
development mode, where Electron may start Django with the developer’s local Python environment
packaged mode, where Electron starts Django from the bundled runtime with production-like local settings
The settings split should make these differences explicit:
development can optimize for debug ergonomics
packaged mode should assume
DEBUG=Falsepackaged mode should use writable per-user data paths
packaged mode should rely on collected static assets rather than Django development conveniences
Data and Files¶
Expected persistence rules:
SQLite is the only database in starter v1
packaged apps write the database under the platform user-data directory, not inside a read-only app bundle
static files are collected as part of packaging, not generated on end-user machines
packaged mode needs an explicit static-file strategy that works when
DEBUG=False
Current expected direction:
use Django-side static file serving in the simplest acceptable form for v1, rather than introducing an additional asset-serving layer in Electron unless it proves necessary
the staged local bundle now mirrors the future packaged layout by keeping the backend payload together and staging the interpreter under
backend/python/app icon source-of-truth now lives under
assets/brand/, with generated Electron outputs written intoshells/electron/assets/icons/, generated Tauri outputs written intoshells/tauri/src-tauri/icons/, and generated Positron outputs written intoshells/positron/resources/the example app base template loads the Play font from Google Fonts via an external stylesheet link; in packaged or air-gapped mode the request will silently fail and the CSS font stack falls back to Helvetica Neue / Arial / sans-serif
Current staged backend contract:
the shared staged backend is materialized under
.stage/backend/at the repo root during local packaged-like buildsbackend/manage.pystays at the bundle rootbackend/src/keeps the normal source layoutbackend/python/contains the bundled interpreter and installed dependenciesbackend/staticfiles/contains collected assets forDEBUG=Falsebackend/runtime-manifest.jsonrecords the interpreter path and launcher metadata
Current launcher contract:
Electron and Tauri packaged mode resolve the interpreter from
backend/runtime-manifest.jsonElectron and Tauri then run
manage.pyfrombackend/for bothrunserveranddb_workerpackaged settings still rely on runtime environment variables for writable app data, bundle dir, localhost host/port, secret key, and unbuffered Python output
Electron, Tauri, and Positron all use the same fallback
DJANGO_SECRET_KEYonly when the environment does not provide one, so local packaged-like startup stays simple without claiming that the fallback value is a release-grade secretpackaged Django settings keep SQLite in per-user app data and now add desktop-oriented SQLite tuning with
transaction_mode=IMMEDIATE, a 20-second timeout,PRAGMA journal_mode=WAL;,PRAGMA synchronous=NORMAL;, and modest cache/mmap settingsthe
/tasks/demo uses the same SQLite database file as the web app, via thedjango_tasks_dbbackend tablesshell-local wrappers such as
shells/electron/scripts/bundled-python.cjsare allowed to resolve shared helpers from two locations: a packaged-app copy first, then a repo-relative source path for local developmentthe Tauri shell keeps its subprocess supervision in Rust under
shells/tauri/src-tauri/src/lib.rsinstead of forcing a cross-shell launcher abstractionthe Tauri shell now also owns its own shell-local splash window under
shells/tauri/src/splash.html, shown while backend startup runs on a background threadthe Positron shell keeps its runtime under
shells/positron/src/desktop_django_starter_positron/, imports the shared Django code from reposrc/, and starts the optional task worker in-process instead of using the Electron/Tauri subprocess contractPositron does not claim packaged splashscreen parity on macOS and does not add a GitHub Actions artifact lane in this slice
Release and Update Model¶
The first implementation does not need a full auto-update system, but it does need a credible release and update story.
connected environments should be able to replace the app using signed release artifacts from a normal release channel
air-gapped environments should be able to move signed artifacts through an approved offline transfer path and install them manually
update docs should describe what artifact an operator moves, how they verify it, and what local state should survive the reinstall
local packaging should still work without release credentials, producing unsigned artifacts for teaching and local validation
the GitHub Actions packaging workflow should consume signing credentials only when they are present, rather than making secrets a baseline requirement for every build
The current Electron connected updater path uses electron-updater, an electron-builder publish config, generated updater metadata, and a Help > Check for Updates... menu action in the Electron main process. It does not add a Django localhost update API or broaden the preload bridge. The release-readiness claim stays intentionally narrower than the implementation: the repo now records a real signed/notarized macOS packaged update dry run performed on April 12, 2026, from installed 0.1.2 to published v0.1.4, proving detection, download, restart/install, and app.sqlite3 persistence, but Electron still needs a real Windows NSIS update dry run before it can be called a release-validated updater lane. Tauri now also has an experimental tauri-plugin-updater path that checks a configured HTTPS endpoint after the first packaged main-window load and relies on signed updater payloads rather than the Django localhost app. Positron currently stays manual-only: the repo documents local macOS DMG replacement rather than a connected updater, because there is no hosted artifact, checksum, or release-publication lane for that shell yet. For air-gapped environments, the baseline is still manual signed artifact replacement rather than background update services.
Native Surface Area¶
The desktop integration should stay narrow:
one preload bridge or application-menu action is enough for v1
the first native action should be simple and generic, such as revealing the app-data folder
no wide IPC API should be exposed to page code
Shutdown Notes¶
Shutdown handling must be treated as a cross-platform lifecycle concern, not as a best-effort cleanup step.
macOS and Linux can usually terminate the Django and task worker child processes with normal process signals
Windows needs explicit handling because process-tree shutdown is less predictable
the current Electron implementation on Windows uses explicit forced child-process tree termination via
taskkill /t /fthat forced tree kill is acceptable for this starter, but it is not the same as a graceful drain, a dedicated local shutdown endpoint, or a Job Object-based production approach
because Windows is a required proof point, reliable shutdown is part of the starter contract
Deferred Areas¶
These are expected later if needed, but not part of the first implementation:
production-grade task orchestration beyond the single supervised
db_workerprocess used bytasks_demoPositron hosted release-lane and connected-updater work beyond the current manual-only strategy
Tauri release publication and stronger updater hosting defaults
multiple windows
richer native integrations