Skip to content

Desktop Shell (Tauri)

Ghost is a desktop application, not a web app — it runs as a native window on your computer with full access to system features like the proxy configuration, certificate store, file system, and system tray. The desktop shell is built with Tauri v2, a framework that wraps a native WebView (the same rendering engine your browser uses) in a Rust process. This gives Ghost the appearance and capabilities of a native app while using React for the user interface.

The architecture has three layers: the Rust shell (Tauri) manages the window, native menus, system tray, and safety mechanisms. The Go backend (called the “sidecar”) runs the proxy, API server, database, and all business logic. The React frontend renders inside the WebView and communicates with the Go backend through HTTP and WebSocket — the same way a web app talks to its server, except everything runs on your local machine.

What this diagram shows — how the three layers interact:

The Tauri shell (Rust) is the outer container that manages everything at the OS level. It creates the window, builds the native menu bar and system tray, runs the safety net watchdog, and handles auto-updates. Inside the window, the React application renders in a WebView — this is the UI the user interacts with. The React app communicates with the Go sidecar through HTTP (REST API) and WebSocket (real-time events). The Go sidecar is a separate process that Tauri spawns as a child — it runs the MITM proxy, serves the API, manages the database, and does all the heavy lifting. The system tray also talks directly to the Go API (to show proxy status and toggle controls). The safety net operates independently of both — if the Go sidecar crashes, it can disable the system proxy using native OS calls without needing the sidecar to be alive.

The Go backend runs as a “sidecar” — a separate executable that Tauri manages as a child process. The binary is named ghost-engine and is bundled inside the application package.

  1. Tauri’s setup hook fires during application initialization
  2. Spawns ghost-engine --sidecar — the Go binary runs with the --sidecar flag, which tells it to print its configuration as JSON to stdout instead of logging to the terminal
  3. Reads stdout line by line, waiting for a JSON object with three fields:
    { "api_port": 5565, "proxy_port": 4545, "token": "abc123..." }
    • api_port — the port the REST API and WebSocket server are listening on
    • proxy_port — the port the MITM proxy is listening on
    • token — an authentication token for API access
  4. 15-second timeout — if the sidecar doesn’t emit valid JSON within 15 seconds, startup fails and the app exits with code 1. This catches scenarios where the binary is missing, crashes on startup, or hangs.
  5. Stores state — the sidecar info and process handle are stored in Tauri’s managed state (GhostState) so other parts of the app can access the port numbers and token
  6. Navigates the WebView — redirects the window from about:blank (its initial URL) to:
    • Debug mode: http://localhost:5173?__ghost_token={token}&__ghost_api_port={api_port} (loads from the Vite dev server for hot module replacement)
    • Release mode: http://localhost:{api_port}?__ghost_token={token}&__ghost_api_port={api_port} (loads from the Go binary itself, which serves the embedded frontend assets)
  7. Drains stderr — a background task reads the sidecar’s stderr output and forwards it to the console, prefixed with [ghost-engine], for debugging

When the user quits Ghost (or the window closes), the shutdown runs in this order:

  1. Disable system proxy natively — calls disable_system_proxy_native() directly (native OS mechanism, does NOT depend on the Go sidecar being alive). This is the most important step — if the sidecar is already dead, this ensures the system proxy doesn’t keep pointing at a dead port.
  2. Try API disable — attempts POST /api/v1/proxy/system/disable with the bearer token, in case the sidecar is still alive and can do a cleaner disable
  3. Kill sidecar — takes the CommandChild from the mutex and calls .kill() to terminate the Go process
ModeWebView URLFrontend SourceWhen Used
Debughttp://localhost:5173?token=...Vite dev server with HMR (hot module replacement — changes appear instantly without page refresh)During development
Releasehttp://localhost:{api_port}?token=...Go-embedded static files (the frontend is compiled into the Go binary via go:embed)Production builds

Both modes pass the authentication token and API port as query parameters so the frontend JavaScript can discover the backend.

PropertyValueWhy
Default size1280 × 800Comfortable for traffic lists with split pane inspector
Minimum size900 × 600Ensures the layout doesn’t break — sidebar + flow list + inspector need at least this much space
Title”Ghost”
Title bar styleOverlay with hidden titleThe title bar area is transparent, allowing the command bar to extend into it. The window title text is hidden (the Ghost logo serves as the visual title).
macOS traffic lightsPosition (16, 20)The close/minimize/maximize buttons are positioned to align with the command bar height. The frontend pads the left side (pl-20, 80px) to avoid overlapping them.
CenteredYesWindow appears in the center of the screen on first launch
ResizableYes

The WebView enforces a strict CSP that limits what the frontend can do:

default-src 'self';
script-src 'self';
connect-src http://localhost:* ws://localhost:*;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data:

What this means:

  • Scripts: Only from the app itself — no external JavaScript can be loaded or injected
  • Connections: Only to localhost — the frontend can only talk to the local Go backend (HTTP and WebSocket), not to any external server
  • Styles: From the app itself plus inline styles (needed for Tailwind’s runtime styling)
  • Images: From the app, data URIs (base64-encoded images), and blob URLs (used for screenshots and generated content)
  • Fonts: From the app and data URIs (the woff2 font files are bundled)
  • No unsafe-eval: The eval() function and its equivalents are blocked

Ghost creates a full native menu bar with 9 menus. On macOS, this appears at the top of the screen (as users expect from native apps). Custom menu actions emit a menu-action event to the frontend with the item’s ID, where the React native menu handler processes it.

ItemAcceleratorNotes
About GhostShows version, copyright “Ghost — AI-Powered Traffic Intelligence”
Settings…Cmd+,Opens the settings modal
HideNative macOS hide
Hide OthersNative macOS hide others
Show AllNative macOS show all
QuitTriggers the shutdown sequence
ItemAcceleratorID
New SessionCmd+Nnew_session
Export Session →Submenu
HARexport_har
JSONexport_json
CSVexport_csv
Postman Collectionexport_postman
Import HAR…import_har
Close WindowNative
ItemAcceleratorID
UndoNative
RedoNative
CutNative
CopyNative
PasteNative
Select AllNative
Command PaletteCmd+Kcmd_palette
Clear Session Flowsclear_flows
ItemID
Request Composerview_composer
AI Chatview_chat
Map Rulesview_rules
Addonsview_addons
Breakpointsview_breakpoints
Toggle Themetoggle_theme
ItemID
Start/Stop Proxyproxy_toggle_menu
Breakpoint Rulestools_breakpoints
Manage Addonstools_addons
ItemAcceleratorID
Bug ReportsCmd+Shift+Btest_bug_reports
Test Scenariostest_scenarios
Generate Bug Reporttest_generate_report
ItemIDNotes
Install Certificate on this Mac/PCcert_install_macLabel is platform-conditional
Install Certificate on iOS →Submenu
Physical Devices…cert_ios_device
Simulators…cert_ios_simulator
Install Certificate on Android →Submenu
Physical Devices…cert_android_device
Emulators…cert_android_emulator
Export Certificate…cert_export
Certificate Statuscert_status

All native items: Minimize, Maximize, Fullscreen.

ItemID
About Ghostabout_ghost

Ghost adds an icon to the operating system’s system tray (macOS menu bar, Windows notification area). The tray provides quick access to proxy controls without needing to open the main window.

ItemInitial StateAction
Open GhostEnabledShows and focuses the main window
Proxy: Starting…DisabledToggles proxy start/stop via the Go API. Label updates to “Proxy: Running” or “Proxy: Stopped” based on status.
System Proxy: Checking…DisabledToggles system proxy enable/disable via the Go API. Label updates to “System Proxy: On” or “System Proxy: Off”.
Quit GhostEnabledCalls app.exit(0), triggering the full shutdown sequence

Tooltip: “Ghost — Traffic Intelligence”

Icon click: Clicking the tray icon (not the menu) shows and focuses the main window.

A background task polls the Go backend every 3 seconds to update the tray menu:

  1. Calls GET /api/v1/proxy/status with bearer auth to check proxy state
  2. Calls GET /api/v1/proxy/system/status to check system proxy state
  3. Emits tray-update events to update menu item labels (e.g., “Proxy: Running” or “Proxy: Stopped”, “System Proxy: On” or “System Proxy: Off”)
  4. Tracks last_sysproxy_enabled for the safety net

The safety net is Ghost’s most important reliability feature. It prevents the catastrophic scenario where Ghost crashes or the Go sidecar dies while the system proxy is still pointing at Ghost’s port — which would break ALL internet connectivity on the user’s machine (every HTTP request would try to connect to a dead proxy port and fail).

The status poller (described above) tracks consecutive failures when trying to reach the Go API. If the API becomes unreachable for 3 consecutive polls (approximately 9 seconds) AND the system proxy was last known to be enabled, the safety net activates:

  1. Calls disable_system_proxy_native() — a Rust function that disables the system proxy using native OS mechanisms, without any help from the Go backend
  2. Resets last_sysproxy_enabled = false to prevent repeated disable attempts

macOS:

  1. Runs networksetup -listallnetworkservices to get all network interfaces
  2. Filters out header lines (lines starting with *)
  3. For each service, runs 4 commands in deliberate order:
    • -setwebproxy {service} "" 0 — clear HTTP proxy address
    • -setsecurewebproxy {service} "" 0 — clear HTTPS proxy address
    • -setwebproxystate {service} off — disable HTTP proxy
    • -setsecurewebproxystate {service} off — disable HTTPS proxy

Why address is cleared before state: Some VPN and security agents (like GlobalProtect) monitor proxy state changes and re-enable their own proxy settings. By clearing the address first, even if a VPN agent re-enables the proxy state, it points to an empty address rather than Ghost’s dead port.

Windows: Sets ProxyEnable = 0 in the Windows registry at HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings using the winreg crate. This is the same registry key that the Windows “Internet Options” dialog uses.

Linux: No-op — Linux proxy configuration is fragmented across GNOME (gsettings), KDE (kwriteconfig), and environment variables, and there’s no single reliable mechanism to disable all of them.

Three functions are exposed from Rust to the frontend JavaScript via Tauri’s IPC bridge:

CommandParametersReturnsPurpose
force_disable_system_proxyNoneNothingEmergency trigger for the safety net. Called by the frontend’s proxy store as a fallback when the Go API is unreachable.
open_pathpath: StringResult<(), String>Opens a directory in the OS file manager (Finder on macOS, Explorer on Windows, xdg-open on Linux). Validates that the path exists and is a directory — rejects files and URLs.
airdrop_certhandle: AppHandleResult<(), String>macOS only. Fetches the .mobileconfig profile from the Go backend (http://localhost:{proxy_port}/ghost-profile.mobileconfig), writes it to a temporary file, then opens the native AirDrop sharing picker via NSSharingService (spawns a Swift subprocess). Used to quickly send the certificate profile to physical iOS devices over AirDrop.

Ghost checks for updates via the Tauri updater plugin, which downloads from a GitLab packages endpoint (Hepsiburada’s internal GitLab instance).

PropertyValue
Plugin@tauri-apps/plugin-updater v2
EndpointGitLab generic packages (/ghost-releases/latest/latest.json)
Windows install modePassive (installs without user interaction after download)
Check timing5 seconds after app launch (to avoid competing with initial data loading)
Check timeout30 seconds

The frontend’s updater store tracks the full lifecycle: checkingavailable (with version info, release notes, date) → downloading (with 0-100% progress) → installed (triggers app relaunch).

PlatformFormatInstall ScopeNotes
macOSDMGUserStandard drag-to-Applications installer
WindowsNSISCurrent userNo admin privileges required (currentUser scope)

The ghost-engine Go binary is bundled as an external binary at binaries/ghost-engine inside the application package.

PluginVersionPurpose
tauri-plugin-shellv2Sidecar process management (spawning ghost-engine), opening external URLs in the default browser
tauri-plugin-dialogv2Native file save/open dialogs for export and import operations
tauri-plugin-fsv2File system access for writing exports, scoped to $APPDATA/**, $DOWNLOAD/**, and $DESKTOP/**
tauri-plugin-updaterv2Auto-update from GitLab packages endpoint
tauri-plugin-processv2Process management — allow-restart (for applying updates) and allow-exit

Tauri v2 uses a capability system to declare exactly what the frontend JavaScript is allowed to do. Ghost defines two capability files:

default.json — Main permissions for the app window:

  • Core window management (set title, show, hide, close, minimize, maximize, focus, center, drag)
  • Shell access restricted to the ghost-engine sidecar only (cannot spawn arbitrary processes)
  • File writes scoped to AppData, Downloads, and Desktop directories only
  • Dialog, updater, and process permissions

localhost.json — Special capability that grants IPC access to the http://localhost:* origin. This is necessary because in release mode, the WebView navigates to the Go backend’s localhost URL (not a Tauri custom protocol), and Tauri’s JS APIs (events, drag, dialog) need explicit permission to work from a remote localhost origin.

FieldValue
Product nameGhost
Bundle identifiercom.ghost.app
Version0.1.0
Rust crateghost-desktop (library: ghost_desktop_lib)
Rust edition2021
WindowsConsole window suppressed in release builds (windows_subsystem = "windows")