# Tauri Django React

> Agents should invoke this skill for Tauri + Django + React desktop apps, especially backend lifecycle, CORS/auth, frontend integration, mandatory light/dark theming, German/English i18n, build packaging, dual desktop/web deployment, Rust commands, and platform-specific gotchas.

- **Type:** Skill
- **Install:** `agentstack add skill-firstp1ck-pi-coding-agent-forge-tauri-django-react`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Firstp1ck](https://agentstack.voostack.com/s/firstp1ck)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Firstp1ck](https://github.com/Firstp1ck)
- **Source:** https://github.com/Firstp1ck/pi-coding-agent-forge/tree/main/pi-skill-tauri-django-react/skills/tauri-django-react

## Install

```sh
agentstack add skill-firstp1ck-pi-coding-agent-forge-tauri-django-react
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Tauri + Django + React Integration Patterns

Dense technical reference for building desktop applications that use Tauri as the native shell, Django as the backend API, and React as the frontend UI. Patterns are designed for reuse across projects.

This skill is self-contained; it must not depend on package-level `docs/` files at runtime.

---

## Quick Start

### Identify the Integration Concern

1. **Which layers are involved?** If the question spans two or more of Rust/Python/TypeScript, this skill applies.
2. **Is it a lifecycle issue?** Port selection, process spawning, health checking, cleanup -> Backend Lifecycle section.
3. **Is it an auth issue?** Cookies not working in Tauri, CORS errors, session tokens -> Cross-Origin Auth section.
4. **Is it a build issue?** PyInstaller fails, bundle missing files, `tauri build` errors -> Build Pipeline section.
5. **Is it a dual-mode issue?** Works in browser but not Tauri (or vice versa) -> Frontend Integration section.
6. **Mandatory local runner:** When creating or updating a project with this skill, add or maintain an executable `scripts/start.sh` that starts both Django and React for local development.
7. **Mandatory frontend UX baseline:** Ask the user for separate light-mode and dark-mode background images before finalizing visual work; implement a persistent light/dark mode setting; implement German/English i18n for every user-facing string.
8. **Mandatory GitHub release automation:** When creating or updating a project with this skill, add or maintain both `.github/workflows/release.yml` and an executable `scripts/release.sh` release helper.
9. **Mandatory configurable update path:** Add or maintain a filesystem update checker that reads a configurable update source directory before checking/installing desktop updates.

### Mandatory GitHub Release Automation

Every Tauri + Django + React project handled by this skill must include GitHub release automation:

- `.github/workflows/release.yml` builds the Tauri desktop app, uploads installer artifacts, and publishes a GitHub Release for `v*` tags or manual `workflow_dispatch` runs.
- `scripts/release.sh` updates project version files, regenerates relevant locks when tooling is available, commits/pushes version changes, creates an annotated `vX.X.X` tag, and pushes the tag to trigger the workflow.
- `dev/RELEASES/` is the canonical release-notes directory; release notes use `dev/RELEASES/RELEASE_vX.X.X.md` and the workflow falls back to the annotated tag message when the file is absent.
- The workflow and script must be adapted to the actual app name, artifact names, package names, secrets, signing/updater setup, and platform targets. Do not copy private updater/Gist/signing logic unless the current project explicitly has that configuration.

### Mandatory Configurable Filesystem Update System

Every desktop-distributed Tauri + Django + React project handled by this skill must include a configurable filesystem update path unless the user explicitly chooses Tauri's built-in updater only:

- **Canonical setting:** `settings.update_source_path` in the app config (`config.json`) stores the local/network directory to scan for installer updates.
- **Config overrides:** support `APP_UPDATE_SOURCE_PATH` or `TAURI_UPDATE_SOURCE_PATH` env vars for deployment overrides; support `APP_CONFIG_PATH` for a custom runtime config file; support `APP_UPDATE_INSTALLER_PATTERNS` for app-specific installer filename regexes.
- **Version source:** Rust passes `TAURI_APP_VERSION` and `APP_VERSION` from `app.package_info().version` into the Django backend. Django falls back to `APP_VERSION`, Django settings, or `pyproject.toml` in development.
- **Installer discovery:** Django scans the configured directory for versioned installer filenames, extracts semantic `X.Y.Z`, compares integer tuples, and returns the newest installer newer than the current version. Keep the current-version installer path too so the UI can offer reinstall.
- **Safety boundary:** only launch installers that exist, are regular files, match the configured filename pattern, and resolve inside the configured update source directory.
- **API contract:** expose `GET /api/updates/check/` for availability, authenticated `GET/PATCH /api/updates/settings/` for the configurable path, and authenticated `POST /api/updates/install/` for launching a validated installer. If `check` is public, redact local/network paths unless the request is authenticated.
- **Frontend contract:** provide an update service and menu/button whose label changes between `Check update` and `Install update`; check at startup and periodically; prompt before launching the installer; close the Tauri app after a successful installer launch.
- **Release artifact contract:** release workflows/scripts must produce installer names that match the checker (for example `-1.2.3-Setup.exe` or Tauri NSIS `_1.2.3_x64-setup.exe`).

### Mandatory `scripts/start.sh` Local Runner

Every Tauri + Django + React project handled by this skill must include `scripts/start.sh` unless the user explicitly declines it. The script is the one-command local development entry point and must:

- use `uv` for backend dependency sync and Django commands;
- use `bun` for frontend dependency install and Vite/React startup;
- run Django migrations before starting the backend;
- optionally run a clearly named seed/demo command when the project has one;
- start Django and React concurrently;
- set `VITE_API_BASE_URL` to the Django `/api` base URL unless already provided;
- print the frontend and backend URLs;
- trap `EXIT`, `INT`, and `TERM` and stop both child processes;
- be executable (`chmod +x scripts/start.sh`).

Recommended template:

```bash
#!/usr/bin/env bash
set -Eeuo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"

BACKEND_HOST="${BACKEND_HOST:-127.0.0.1}"
BACKEND_PORT="${BACKEND_PORT:-8000}"
FRONTEND_URL="http://localhost:5173"
BACKEND_URL="http://${BACKEND_HOST}:${BACKEND_PORT}"

backend_pid=""
frontend_pid=""

cleanup() {
  echo
  echo "Stopping application..."
  if [[ -n "$frontend_pid" ]] && kill -0 "$frontend_pid" 2>/dev/null; then
    kill "$frontend_pid" 2>/dev/null || true
  fi
  if [[ -n "$backend_pid" ]] && kill -0 "$backend_pid" 2>/dev/null; then
    kill "$backend_pid" 2>/dev/null || true
  fi
  wait 2>/dev/null || true
}

trap cleanup EXIT INT TERM

(
  cd "$BACKEND_DIR"
  uv sync --all-groups
  uv run python manage.py migrate
  if uv run python manage.py help seed_demo >/dev/null 2>&1; then
    uv run python manage.py seed_demo
  fi
)

(
  cd "$FRONTEND_DIR"
  bun install
)

(
  cd "$BACKEND_DIR"
  uv run python manage.py runserver "${BACKEND_HOST}:${BACKEND_PORT}"
) &
backend_pid=$!

(
  cd "$FRONTEND_DIR"
  VITE_API_BASE_URL="${VITE_API_BASE_URL:-${BACKEND_URL}/api}" bun run dev
) &
frontend_pid=$!

echo "Frontend: $FRONTEND_URL"
echo "Backend:  $BACKEND_URL/api"
echo "Press Ctrl+C to stop both processes."

wait -n "$backend_pid" "$frontend_pid"
```

---

## Backend Lifecycle (Tauri / Rust)

The core responsibility of the Rust layer: start the Django backend, confirm it's healthy, tell the frontend, and clean up on exit.

### Port Selection

```rust
const DEFAULT_BACKEND_PORT: u16 = 8000;
const MAX_PORT_OFFSET: u16 = 10;

fn find_available_port() -> Option {
    for offset in 0..=MAX_PORT_OFFSET {
        let port = DEFAULT_BACKEND_PORT + offset;
        if TcpListener::bind(("127.0.0.1", port)).is_ok() {
            return Some(port);
        }
    }
    None
}
```

- Probes 8000-8010 using `TcpListener::bind()` -- bind succeeds means port is free
- The selected port is passed to Django via `BACKEND_PORT` env var and to React via a Tauri event
- Never hardcode the port in frontend code

### Subprocess Spawning

```rust
let mut cmd = Command::new(&backend_path);
cmd.current_dir(&backend_dir);
cmd.env("BACKEND_PORT", port.to_string());
cmd.env("DJANGO_DATABASE_PATH", db_path.to_string_lossy().to_string());
cmd.env("TAURI_APP_DATA_DIR", app_data.to_string_lossy().to_string());
cmd.env("TAURI_APP_VERSION", app.package_info().version.to_string());
cmd.env("APP_VERSION", app.package_info().version.to_string());

#[cfg(target_os = "windows")]
{
    use std::os::windows::process::CommandExt;
    cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
}

let child = cmd.spawn()?;
```

- Backend path resolved from `resource_dir/python-backend/tauri_entry` (+ `.exe` on Windows)
- `CREATE_NO_WINDOW` (0x08000000) prevents a visible console window on Windows
- Pass the Tauri package version to Django via `TAURI_APP_VERSION`/`APP_VERSION` so update checks compare against the installed app version
- Store the `Child` handle in `Mutex>` for later cleanup
- On Windows, strip UNC `\\?\` prefix from paths before passing to Python

### Health Polling

```rust
let client = reqwest::blocking::Client::builder()
    .timeout(Duration::from_secs(2))
    .build()?;

for attempt in 0..max_retries {
    match client.get(&format!("http://127.0.0.1:{}/api/health/", port)).send() {
        Ok(resp) if resp.status().is_success() => {
            app_handle.emit("backend-ready", json!({ "port": port }))?;
            return Ok(());
        }
        _ => std::thread::sleep(Duration::from_millis(500)),
    }
}
app_handle.emit("backend-error", json!({ "error": "Backend failed to start" }))?;
```

- Poll `GET /api/health/` with a 2-second timeout per request
- Retry with 500ms sleep between attempts (typically 30-60 retries = 15-30 seconds max wait)
- On success: emit `backend-ready` with the port number
- On failure: emit `backend-error` with a description

### Process Cleanup

```rust
fn kill_backend_process(state: &Mutex>) {
    if let Some(mut backend) = state.lock().unwrap().take() {
        #[cfg(target_os = "windows")]
        {
            // taskkill /F /T kills the entire process tree
            let _ = std::process::Command::new("taskkill")
                .args(["/F", "/T", "/PID", &backend.child.id().to_string()])
                .creation_flags(0x08000000)
                .output();
        }
        #[cfg(not(target_os = "windows"))]
        {
            let _ = backend.child.kill();
        }
    }
}
```

- **Windows:** Must use `taskkill /F /T /PID` to kill the entire process tree (PyInstaller spawns child processes)
- **Unix:** `child.kill()` sends SIGKILL, which is sufficient
- Call cleanup on: app exit, window close, `before_exit` event, and `on_window_event(CloseRequested)`

### Window Lifecycle

- **Windows:** Minimize to system tray on close (`api.prevent_close()` + `window.hide()`)
- **Linux/macOS:** Quit the application on window close (standard behavior)
- Tray icon re-shows the window on click

### Event Emission Summary

| Event | Payload | When |
|---|---|---|
| `backend-ready` | `{ "port": number }` | Health check passes |
| `backend-error` | `{ "error": string }` | Health check exhausts retries, or spawn fails |

---

## Cross-Origin Authentication (Django / Python)

WebView in Tauri runs from `tauri://localhost` (or `https://tauri.localhost`), which means cookies may not work reliably for `http://127.0.0.1:8000`. The solution: a hybrid auth system.

### HybridSessionAuthentication

```python
class HybridSessionAuthentication(authentication.SessionAuthentication):
    def authenticate(self, request):
        auth_header = authentication.get_authorization_header(request)
        if auth_header:
            auth_parts = auth_header.decode("utf-8").split()
            if auth_parts and auth_parts[0].lower() == "session":
                token_auth = SessionTokenAuthentication()
                return token_auth.authenticate(request)
        return super().authenticate(request)
```

- **Priority 1:** Check for `Authorization: Session ` header (Tauri path)
- **Priority 2:** Fall back to standard cookie-based session auth (browser path)
- Register in DRF settings: `DEFAULT_AUTHENTICATION_CLASSES = ["api.authentication.HybridSessionAuthentication"]`

### SessionTokenAuthentication

```python
class SessionTokenAuthentication:
    def authenticate(self, request):
        session_key = self._extract_key(request)
        session = SessionStore(session_key=session_key)
        if not session.exists(session_key):
            raise AuthenticationFailed("Invalid session")
        user_id = session.get("_auth_user_id")
        user = User.objects.get(pk=user_id)
        session["_session_expiry"] = settings.SESSION_COOKIE_AGE  # sliding expiration
        session.save()
        return (user, None)
```

- Looks up the session in Django's session store using the key from the header
- Implements sliding expiration by resetting `_session_expiry` on each authenticated request

### CORS Configuration

```python
CORS_ALLOWED_ORIGIN_REGEXES = [
    r"^tauri://localhost$",
    r"^https?://tauri\.localhost$",
    r"^https?://localhost(:\d+)?$",
    r"^https?://127\.0\.0\.1(:\d+)?$",
]
CORS_ALLOW_CREDENTIALS = True
```

### CSRF Configuration

```python
CSRF_TRUSTED_ORIGINS = [
    "tauri://localhost",
    "https://tauri.localhost",
    "http://localhost",
    "http://127.0.0.1",
] + [f"http://localhost:{p}" for p in range(8000, 8011)]
  + [f"http://127.0.0.1:{p}" for p in range(8000, 8011)]
CSRF_COOKIE_SAMESITE = "Lax"
```

- Include the full port range (8000-8010) to handle dynamic port selection
- `CORS_ALLOW_CREDENTIALS = True` is required for session cookies in web mode

### Health Endpoint

```python
@api_view(["GET"])
@permission_classes([AllowAny])
def health_check(_request):
    return Response({"status": "ok"}, status=200)
```

- Must be unauthenticated (`AllowAny`) -- Tauri polls this before any user login
- URL: `GET /api/health/`
- Keep the response minimal for fast polling

---

## Frontend Integration (React / TypeScript)

### Mandatory Theme + Background + i18n Baseline

Every React frontend created or substantially updated with this skill must include these UI foundations unless the user explicitly declines them:

1. **Ask for backgrounds first:** Before finalizing visual styling, ask the user whether they have separate light-mode and dark-mode background images. If supplied, store them under `frontend/src/assets/` with stable names such as `background_light.png` and `background_dark.png`; if not supplied, use temporary CSS gradients and leave a clear TODO.
2. **Persistent light/dark setting:** Implement a `Theme = "light" | "dark"` state, initialize it from `localStorage`, fall back to `prefers-color-scheme`, write `document.documentElement.dataset.theme`, set `document.documentElement.style.colorScheme`, and expose a translated toggle button.
3. **Theme-aware backgrounds:** Wire backgrounds through CSS selectors (`body` for light, `:root[data-theme='dark'] body` for dark). Use overlays/gradients above the images so text contrast remains acceptable in both modes.
4. **German/English i18n:** Implement `Language = "en" | "de"`, complete dictionaries for both languages, a typed `translate()`/`t()` helper with parameter interpolation, language detection from `localStorage` and `navigator.language`, a language switcher, and `document.documentElement.lang` + `document.title` updates.
5. **No hardcoded UI strings:** All visible labels, buttons, status messages, errors, ARIA labels, document titles, and theme/language controls must use i18n keys in both English and German.

Recommended theme pattern:

```typescript
type Theme = "light" | "dark";
const themeStorageKey = "app-theme";

function getInitialTheme(): Theme {
  const stored = window.localStorage.getItem(themeStorageKey);
  if (stored === "light" || stored === "dark") return stored;
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}

useEffect(() => {
  document.documentElement.dataset.theme = theme;
  document.documentElement.style.colorScheme = theme;
  window.localStorage.setItem(themeStorageKey, theme);
}, [theme]);
```

Recommended ba

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Firstp1ck](https://github.com/Firstp1ck)
- **Source:** [Firstp1ck/pi-coding-agent-forge](https://github.com/Firstp1ck/pi-coding-agent-forge)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** yes
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-firstp1ck-pi-coding-agent-forge-tauri-django-react
- Seller: https://agentstack.voostack.com/s/firstp1ck
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
