|
| 1 | +# Deployment and Threading |
| 2 | + |
| 3 | +Cursor Chat Browser is a **local, single-user** tool for reading Cursor chat history. It binds to `127.0.0.1` by default and is not designed as a multi-tenant internet-facing service. This page documents supported WSGI configurations, threading guarantees, and path-configuration trust boundaries. |
| 4 | + |
| 5 | +## Quick start (production WSGI) |
| 6 | + |
| 7 | +Install runtime dependencies, then serve with gunicorn (Linux/macOS) or waitress (cross-platform): |
| 8 | + |
| 9 | +```bash |
| 10 | +pip install -r requirements-lock.txt # or: pip install -e . |
| 11 | +pip install gunicorn # Linux / macOS |
| 12 | +# pip install waitress # Windows-friendly alternative (see below) |
| 13 | + |
| 14 | +# Multi-process (recommended): one thread per worker avoids any per-worker state surprises. |
| 15 | +gunicorn --factory --bind 127.0.0.1:3000 --workers 2 --threads 1 app:create_app |
| 16 | + |
| 17 | +# Single-process, multi-threaded: safe after the #43 lock; useful for lighter deployments. |
| 18 | +gunicorn --factory --bind 127.0.0.1:3000 --workers 1 --threads 4 app:create_app |
| 19 | +``` |
| 20 | + |
| 21 | +**Waitress** (works well on Windows): |
| 22 | + |
| 23 | +```bash |
| 24 | +pip install waitress |
| 25 | +python -c "from waitress import serve; from app import create_app; serve(create_app(), host='127.0.0.1', port=3000, threads=4)" |
| 26 | +``` |
| 27 | + |
| 28 | +Set `WORKSPACE_PATH` (and optionally `CLI_CHATS_PATH`) in the environment **before** starting the server when the data directory is known at launch time — see [Path configuration](#path-configuration) below. |
| 29 | + |
| 30 | +Do **not** enable Flask debug mode in production (`--debug` / `FLASK_DEBUG=1`). The Werkzeug debugger allows arbitrary code execution by anyone who can reach the port. |
| 31 | + |
| 32 | +## Development server |
| 33 | + |
| 34 | +```bash |
| 35 | +python app.py |
| 36 | +``` |
| 37 | + |
| 38 | +This uses Flask/Werkzeug's built-in server. It is **single-threaded by default** (`threaded=False`), which is appropriate for local development. Requests are handled one at a time; there is no concurrent access to in-process state. |
| 39 | + |
| 40 | +Werkzeug's reloader is disabled on Windows to avoid socket conflicts; on other platforms it follows the debug flag. |
| 41 | + |
| 42 | +## Threading guarantee |
| 43 | + |
| 44 | +The web app keeps one piece of shared mutable state: the module-level workspace path override in `utils/workspace_path.py`, written by `POST /api/set-workspace` (or `--base-dir` at startup). Reads and writes are serialized with `threading.Lock` (issue #43), so **multi-threaded deployment within a single OS process is supported**. Regression tests in `tests/test_workspace_path_thread_safety.py` exercise concurrent set/resolve under thread pools. |
| 45 | + |
| 46 | +| Concurrency model | Supported? | Notes | |
| 47 | +|-------------------|------------|-------| |
| 48 | +| Single-threaded (dev server, `--threads 1`) | Yes | Simplest; default for `python app.py`. | |
| 49 | +| Multi-threaded, single process (`--workers 1 --threads N`, waitress) | Yes | Override protected by lock; safe for concurrent API requests. | |
| 50 | +| Multi-process (`--workers N`, N > 1) | Yes, with caveats | Each worker is a separate process with **its own memory**. See [Multi-process deployments](#multi-process-deployments). | |
| 51 | + |
| 52 | +SQLite reads are opened per request and closed via context managers; connections are not shared across threads. |
| 53 | + |
| 54 | +## WSGI servers |
| 55 | + |
| 56 | +| Server | Role | Threading notes | |
| 57 | +|--------|------|-----------------| |
| 58 | +| **Werkzeug** (via `python app.py`) | Local development | Single-threaded by default; safe without extra configuration. | |
| 59 | +| **gunicorn** | Production (Linux/macOS) | Use `--factory app:create_app`. Prefer `--workers N --threads 1` for multi-process, or `--workers 1 --threads N` for threaded single-process. | |
| 60 | +| **waitress** | Production (all platforms, especially Windows) | Multi-threaded by default; compatible with the workspace-path lock. | |
| 61 | +| **pywebview** (desktop `.exe`) | Desktop GUI | No HTTP server or port; calls the WSGI app in-process. Request handling is serialized by the embedded server — equivalent to single-threaded from the app's perspective. | |
| 62 | + |
| 63 | +gunicorn and waitress are **not** runtime dependencies; install them only when deploying behind a production WSGI server. |
| 64 | + |
| 65 | +## Multi-process deployments |
| 66 | + |
| 67 | +When gunicorn runs with `--workers 2` (or more), each worker is an independent Python process: |
| 68 | + |
| 69 | +- **`POST /api/set-workspace`** only updates the worker that handled that request. Other workers keep their previous override (usually `None`). Prefer setting `WORKSPACE_PATH` or passing `--base-dir` at process start so every worker sees the same path. |
| 70 | +- **Exclusion rules** (`EXCLUSION_RULES` in app config) are loaded once at worker startup from `--exclude-rules` or the default file. Changing the rules file requires restarting workers. |
| 71 | + |
| 72 | +For a single-user localhost deployment with multiple workers, set the workspace path via environment variable rather than the Configuration page. |
| 73 | + |
| 74 | +## Path configuration |
| 75 | + |
| 76 | +Workspace path resolution order: **runtime override** (`POST /api/set-workspace` or `--base-dir`) → **`WORKSPACE_PATH` environment variable** → **OS auto-detection** (see README Configuration table). |
| 77 | + |
| 78 | +### Trust boundaries |
| 79 | + |
| 80 | +| Mechanism | Validation | Intended use | |
| 81 | +|-----------|------------|--------------| |
| 82 | +| `POST /api/set-workspace`, `POST /api/validate-path` | Canonical path (`realpath`), directory checks, Cursor workspace markers (`state.vscdb` in immediate subdirectories) | Web UI and API callers; safe for interactive use. | |
| 83 | +| `WORKSPACE_PATH` env var | Tilde expansion only (`~` → home directory) | **Trusted-operator** escape hatch for automation, systemd units, and containers where the path is already known good. Not a substitute for API validation when input may be untrusted. | |
| 84 | +| `--base-dir` CLI flag | None (passed through to the same override as set-workspace) | Startup override for operators who control the launch command. | |
| 85 | +| OS auto-detection | N/A | Default when no override or env var is set. | |
| 86 | + |
| 87 | +`CLI_CHATS_PATH` follows the same “tilde-expanded env var, trusted operator” model for Cursor CLI agent sessions under `~/.cursor/chats/`. |
| 88 | + |
| 89 | +## Desktop mode (pywebview) |
| 90 | + |
| 91 | +The Windows desktop build (`cursor-browser.spec` / `CursorChatBrowser.exe`) embeds the Flask app via [pywebview](https://pywebview.flowrl.com/). The UI is rendered in a native window (Edge WebView2 on Windows); no HTTP port is opened. Threading semantics match a single-threaded in-process WSGI server. |
| 92 | + |
| 93 | +Install the desktop extra when building locally: `pip install -e ".[desktop]"`. |
| 94 | + |
| 95 | +## CLI export (no web server) |
| 96 | + |
| 97 | +`cursor-chat-export` / `python scripts/export.py` does not start Flask and has no threading or WSGI concerns. It reads `WORKSPACE_PATH` from the environment (or platform defaults) in a single process. |
| 98 | + |
| 99 | +## Known limitations |
| 100 | + |
| 101 | +- **Localhost-first**: Default bind address is `127.0.0.1`. Exposing the app on `0.0.0.0` without additional access controls is discouraged. |
| 102 | +- **No authentication**: The API assumes a trusted local operator. |
| 103 | +- **Exclusion rules**: Loaded at app startup; restart required after editing the rules file. |
| 104 | +- **Read-only data access**: The app reads Cursor's SQLite databases; it does not write to Cursor storage. |
0 commit comments