Skip to content

App Architecture

Ghost’s frontend is a single-page React 19 application with no router — there are no separate “pages” or URL paths to navigate between. Everything lives on one screen. The traffic list is always visible, and every other feature (AI chat, map rules, addons, settings) appears as an overlay panel that slides in from the right side. When you select a flow, the traffic list shrinks and a detail inspector appears next to it. This design ensures you never lose sight of the traffic — the data you’re working with is always right there.

Think of it like a desktop email client: the inbox (traffic list) is always on screen, clicking an email (flow) opens a preview pane beside it, and toolbars (slide-over panels) open on the side without navigating away. There’s no back button, no page transitions, no loading screens between views.

What this diagram shows — how the entire UI is structured as a tree of components:

The App component is the root of everything. When Ghost first launches, it checks whether the first-run setup has been completed (installing the root certificate and enabling the proxy). While checking, it shows a pulsing ghost logo. If setup hasn’t been completed, it shows the SetupWizard. Once setup is done, it renders the AppShell — the main application layout.

The AppShell has three permanent zones stacked vertically: the CommandBar at the top (38px tall, containing the logo, session picker, mode toggle, proxy button, search bar, and toolbar icons), the main content area in the middle (which includes the left sidebar, the traffic/content view, and an optional slide-over panel), and the StatusBar at the bottom (28px tall, showing proxy status, flow rate, connection health, and resource usage).

The main content area is wrapped in a DropZone that accepts HAR and JSON file drops for session import. Inside it, the left sidebar (240px wide, collapsible) shows either the ScopePanel (domain navigator, app list, device list) in QA mode or the FindingsPanel (security findings and target configuration) in security mode.

The center area uses a single-slot routing system (activePanel in the UI store). When activePanel is null, the default traffic view is rendered. When it’s set to a panel ID (e.g., 'comparison', 'inspector', 'attack-planner'), the corresponding component from PANEL_COMPONENTS replaces the traffic view entirely. Only one exclusive panel can be active at a time — mutual exclusion is inherent because there’s a single slot. Each exclusive panel has a <BackButton /> that shows the previous panel’s label (e.g., ”← Attack Planner” or ”← Traffic”) and restores it via a closure-based snapshot.

Panel definitions live in stores/panel-registry.ts. Adding a new exclusive panel requires: (1) an entry in the registry, (2) a component in PANEL_COMPONENTS, (3) calling openPanel(id), and (4) adding <BackButton /> to the panel header.

The traffic view has a FilterStrip at the top (preset buttons, content type pills, and a visual query builder) and either a full-width flow list (when no flow is selected) or a resizable split panel (flow list at 55% + flow inspector at 45%) when a flow is selected.

On the right side, a SlideOver panel can appear as an overlay (420px default width, resizable from 320px to 70% of the viewport). Only one slide-over is visible at a time — opening a new one replaces the current one.

Several overlay components float above everything: the settings modal, about dialog, setup guide, breakpoint editor, bug report overlay, comparison flow picker, and command palette.

┌──────────────────────────────────────────────────────────────────┐
│ CommandBar (38px) │
│ [logo] [session] [QA|Security] [proxy] | [search + filters] | [toolbar icons] │
├──────┬───────────────────────────────────────────┬───────────────┤
│ Side │ FilterStrip (presets + content types) │ SlideOver │
│ bar │ ┌──────────────┬────────────────┐ │ Panel │
│ 240px│ │ Flow List │ Flow Inspector│ │ 420px │
│ │ │ (55%) │ (45%) │ │ (resizable │
│ │ │ min: 20% │ min: 15% │ │ 320-70vw) │
│ │ │ │ │ │ │
│ │ └──────────────┴────────────────┘ │ │
├──────┴───────────────────────────────────────────┴───────────────┤
│ StatusBar (28px) │
│ [proxy●port] [flow rate] [throttle] | [X flows] [session] | [ext] [ws●] [db/mem] │
└──────────────────────────────────────────────────────────────────┘

The split between the flow list and inspector uses react-resizable-panels with draggable dividers. The panel orientation can be switched between horizontal (side-by-side, the default) and vertical (stacked top-bottom) through a layout toggle. Panel sizes are percentage-based — the flow list defaults to 55% with a minimum of 20%, and the inspector defaults to 45% with a minimum of 15%.

The left sidebar uses a fixed 240px CSS width (not the resizable panels library) and can be collapsed entirely via Ctrl+B / Cmd+B.

When the app loads after setup is complete, it runs through a specific sequence of operations. Understanding this sequence helps explain why certain things appear before others and what happens if any step fails.

  1. Register global keyboard shortcuts — sets up all Ctrl+1 through Ctrl+5 shortcuts, Ctrl+B, Ctrl+P, etc. These are active immediately.

  2. Register native menu handler — connects to Tauri’s native macOS/Windows menu bar events (File > Export, Tools > Addons, etc.). This enables the operating system menu to control Ghost.

  3. Initialize toast bridge — subscribes to error fields on 5 stores (flows, sessions, addons, proxy, settings). When any store sets an error, a toast notification appears automatically. Also watches proxy status changes to show “Proxy started on :PORT” and “Proxy stopped” toasts. Includes deduplication (5-second window, 100-entry max cache) and suppresses “Backend unavailable” messages.

  4. Check setup status — calls the backend to verify the first-run wizard was completed. Until this returns, the app shows a loading spinner.

  5. Fetch initial data (all in parallel after setup confirmed):

    • Fetch all sessions from the database
    • Fetch proxy status (running, stopped, port)
    • Fetch all addons
    • Fetch breakpoint rules
    • Fetch map rules
    • Fetch injection rules
    • Fetch connected devices
  6. Check for updates (5-second delay) — after a 5-second setTimeout, checks the Tauri updater for a new version. If an update is available, shows a toast notification that stays visible for 15 seconds with a link to Settings > About.

  7. Initialize Sentry + telemetry — fetches settings from the backend. If a Sentry DSN is configured, initializes the Sentry SDK for error tracking (10% trace sample rate, production environment only, strips auth tokens from breadcrumb URLs). If telemetry is enabled, sends a session_start heartbeat and registers a beforeunload handler to send session_end.

  8. Establish WebSocket connection — connects to the backend’s WebSocket endpoint for real-time event streaming. This is how the traffic list updates in real time.

  9. Fetch flows — loads the initial set of flows based on the active session, filters, and scope selections.

  10. Polling fallback — if the WebSocket disconnects, a setInterval polls for new flows every 3 seconds until the WebSocket reconnects. This ensures the traffic list stays somewhat current even during connection interruptions.

Ghost uses a single persistent WebSocket connection to the Go backend for all real-time updates. Every time a new flow is captured, a rule is changed, a device connects, or any other state change occurs, the backend broadcasts an event through this connection and the frontend updates immediately.

The WebSocket URL is constructed from the backend’s HTTP URL:

  • In Tauri (desktop app): converts the http:// base URL to ws:// and appends /ws?token=<encoded_token>
  • In browser (Go-served frontend): derives from window.location using wss: for HTTPS or ws: for HTTP

If no authentication token is available yet (can happen briefly during Tauri startup), the connection retries after 500ms.

When the WebSocket connection drops (backend restart, network interruption), it automatically reconnects with exponential backoff:

AttemptDelay
1st1 second
2nd2 seconds
3rd4 seconds
4th8 seconds
5th16 seconds
6th+30 seconds (cap)

On successful reconnection, the delay resets back to 1 second for the next potential disconnection. The connection status is tracked as one of three states: connecting, connected, or disconnected.

38 distinct event types are handled, routed to the appropriate Zustand store:

CategoryEventsTarget StoreWhat Happens
Flowsflow.created, flow.updated, flow.deletedflowStoreNew flows are checked against active filters before being added to the list. Deleted events can clear individual flows or all flows. Stats (host counts, app counts, device counts) are refreshed with a 2-second debounce after flow events.
Sessionssession.created, session.updated, session.deletedsessionStoreSessions are upserted (created or updated) in the session list. Deleted sessions are removed.
Proxyproxy.started, proxy.stopped, proxy.statusproxyStoreUpdates the proxy status indicator (green dot = running, gray = stopped).
Addonsaddon.created, addon.updated, addon.deleted, addon.logaddonStoreAddon CRUD updates the addon list. Log events append to the addon console (used for ghost.log(), ghost.warn(), ghost.error() output from addon scripts).
Extensionextension.connected, extension.disconnected, extension.interaction, extension.tab_switch, extension.console_error, extension.navigationextensionStoreConnection events update the extension status indicator. Interactions are mapped from camelCase (backend) to snake_case (frontend DTO). Console errors and navigation events are received but not currently surfaced in the UI.
Breakpointsflow.breakpoint, flow.resumedbreakpointStoreBreakpoint events add pending flows to the breakpoint editor. Resume events remove them.
Rulesrule.created, rule.updated, rule.deleted, injection_rule.created, injection_rule.updated, injection_rule.deletedrulesStore, injectionRulesStoreCRUD updates for map rules and injection rules.
Devicesdevice.discovered, device.updated, device.connected, device.disconnected, device.removeddeviceStoreAll device events upsert the device in the store, except removed which deletes it.
Artifactsartifact.created, artifact.deletedartifactStoreBug report and export artifact CRUD.
Findingsfinding.created, finding.updated, finding.deleteduiStoreBumps a version counter that triggers re-fetches. For critical/high severity findings in security mode, shows a toast notification.
Fridafrida.session.started, frida.session.stopped, frida.outputfridaStoreSession events trigger a re-fetch of Frida sessions. Stop events show a retry toast if the user had an active operation. Output events append to the Frida console.
Attackerattacker:started, attacker:progress, attacker:completed, attacker:errorattackerStoreRequest attacker lifecycle events: baseline, progress updates, summary, and errors.

When a flow.created event arrives, it doesn’t automatically appear in the traffic list. The flow is first checked against all active filters using matchesClientFilter(). This function checks, in order:

  1. Method filter — case-insensitive match against the HTTP method
  2. Status code filter — supports exact codes (404), ranges (2xx, 3xx), and comparisons (>399, <500)
  3. Host filter — substring match from structured filter conditions
  4. Content type filter — substring match from structured filter conditions
  5. Domain navigator selection — exact match against the selected domain in the left sidebar
  6. Content type panel selection — substring match against the selected content category
  7. App navigator selection — exact match against the selected app’s source_app field
  8. Device navigator selection — matches against device_id or client_ip
  9. Free text search — case-insensitive substring search across method, host, path, and url

Only flows that pass ALL active filters are added to the visible list. This means the live traffic feed respects your current view — if you’re filtering to “status:>399”, you’ll only see error responses appear in real time.

Ghost has two operational modes — QA and Security — that change which components are rendered in several zones of the interface. The mode is stored in useUIStore.appMode and toggled via a pill button pair in the command bar.

UI ZoneQA ModeSecurity Mode
Left sidebarScopePanel — domain navigator tree, app list, device list with selection for scoping the traffic viewFindingsPanel — security finding summary, target patterns, severity stats
Flow listFlowList — standard traffic list with status/method/host/path columnsSecurityFlowList — traffic list with security-focused columns and finding indicators
Flow inspectorFlowInspector — request/response detail with headers, body, timing, notesSecurityInspector — request/response detail with security finding annotations
Center view optionN/ASecurityFindingsView — full-width findings table that replaces the flow list when selected
Command bar iconsStandard toolbar (compose, chat, rules, injection, addons, extension, compare, settings)Standard toolbar + Request Attacker (crosshair icon) + Frida Instrumentation (syringe icon)

When switching modes, the UI resets: slide-over panel closes, search query clears, all filters reset, all selections (hosts, apps, devices, content types) clear, inspector closes, Frida panel closes, sort state resets, flow selection clears, and the security center view resets. This clean slate ensures you start fresh in the new mode without leftover filter state from the previous mode.

Exclusive center panels: Five panels can replace the traffic view: Attack Planner, Session Compare, Inspector, Frida, and Security Findings. These are mutually exclusive via the single activePanel slot in the UI store — only one can be active at a time. Opening any panel automatically replaces whatever was there. Each panel gets a context-aware back button that navigates to the previous panel (or traffic).

The right side of the screen can show a slide-over panel — an overlay that appears on top of the traffic view without replacing it. Only one slide-over is visible at a time. Opening a new panel replaces the current one.

Panel IDTitleWhat It Contains
chatAI ChatAI agent conversation interface with streaming responses
rulesMap RulesMap rules editor — create, edit, enable/disable URL rewriting and response modification rules
injection-rulesInjection RulesJavaScript injection rules — manage scripts that get injected into web pages
addonsAddonsAddon editor with Monaco code editor, template gallery, and real-time log console
breakpointsBreakpoint RulesBreakpoint rule configuration — set conditions for pausing requests/responses
composerRequest ComposerManual HTTP request builder with headers, body, and response comparison
bug-reportsBug ReportsList of generated bug reports with detail view
test-scenariosTest ScenariosGenerated test scenario viewer
security-toolsSecurity ToolsExternal security scanner management (install, enable, configure tools like sqlmap, nikto)
attackerRequest AttackerRequest fuzzer configuration and results (security mode only)
extensionBrowser ExtensionBrowser extension connection status, captured interactions, and action controls

Panel dimensions:

  • Default width: 420px
  • Minimum width: 320px
  • Maximum width: 70% of viewport width
  • Resizable via a drag handle on the left edge
  • Header height: 44px (h-11)
  • Positioned as an absolute overlay (z-40) on the right side of the main content area

The panel includes scroll-pinning logic to prevent child components that call scrollIntoView() from shifting the panel’s scroll position unexpectedly.

The command bar is the top strip of the application (38px tall) containing all primary navigation and action controls. It has a draggable region at the very top (for moving the Tauri window) and is padded on the left (pl-20, 80px) to avoid overlapping with macOS traffic light buttons (close/minimize/maximize).

Left-to-right contents:

  1. Logo — “GHOST” in gradient text (cyan → purple → pink)
  2. Session picker — dropdown showing the active session name with a caret. Click to see all sessions; option to create a new session.
  3. Mode toggle — two pill buttons: QA and Security. Switching resets all filters and selections.
  4. Proxy toggle — button showing proxy state: “Capturing” (with port), “Not Capturing”, or “Stopped”. Click to start/stop.
  5. Divider
  6. Search bar — magnifying glass icon, active filter chips (removable), text input field. Debounced at 600ms. Shows syntax hint dropdown when typing filter prefixes (method:, status:, host:, content_type:, duration:, size:).
  7. Divider
  8. Toolbar icons (left-to-right):
    • Compose Request (paper plane icon)
    • AI Chat (chat bubble icon)
    • Map Rules (shuffle icon)
    • Script Injection (code block icon)
    • Addons (puzzle piece icon)
    • Browser Extension (globe icon)
    • Compare Sessions (git diff icon)
    • Security mode only: Request Attacker (crosshair icon), Frida (syringe icon)
    • Settings (gear icon)
    • Divider
    • Theme toggle (sun/moon icon)

The status bar is the bottom strip (28px tall) with a subtle gradient accent line at the top (cyan → purple → pink, 30% opacity). It’s divided into three sections:

Left section:

  • Proxy indicator — colored dot (green pulsing when running, amber pulsing when network is down, gray when stopped) with the port number. Clickable to toggle proxy.
  • Flow rate — rolling 5-second window counter showing requests per second (e.g., “2.4/s”). Only visible when the proxy is running and the rate is above zero.
  • Throttle dropdown — gauge icon that opens a dropdown with network throttling presets and custom speed/latency inputs.

Center section:

  • Flow count — “X flows” with aria-live="polite" for screen readers
  • Active filter badge — shows how many filters are active, with a click-to-clear button
  • Active session name

Right section:

  • Extension status — dot indicator and “Extension” label, only shown after the first extension connection. Clickable to open the extension panel.
  • WebSocket status — green/amber/red dot with “Live”, “Connecting…”, or “Disconnected” label
  • Resource monitor — database size and memory allocation, polled every 30 seconds. Only visible on screens wider than the sm breakpoint.
  • Ctrl+K hint — small button that dispatches a keyboard event to open the command palette

Opened with Ctrl+K / Cmd+K. A search-powered command launcher that fuzzy-matches against available commands. Navigate with arrow keys, execute with Enter, close with Escape.

Available commands:

CommandActionShortcut Displayed
Open AI ChatOpens the chat slide-over panelCtrl+4
Open Map RulesOpens the map rules slide-over panelCtrl+3
Open AddonsOpens the addons slide-over panelCtrl+5
Open BreakpointsOpens the breakpoint rules panel
Close PanelCloses any open slide-over panel
Open SettingsOpens the settings modalCtrl+,
Start/Stop ProxyToggles the proxy (label changes based on state)Ctrl+P
Toggle Scope PanelShows/hides the left sidebar
Switch to Light/Dark ModeToggles the theme
Clear All FlowsDeletes all flows in the active session
New SessionCreates a new session with a timestamp name

All shortcuts are suppressed when focus is inside an INPUT, TEXTAREA, SELECT, or contentEditable element (except Ctrl+F which always works).

ShortcutAction
Ctrl+1 / Cmd+1Close slide-over panel, focus on traffic
Ctrl+3 / Cmd+3Open Map Rules panel
Ctrl+4 / Cmd+4Open AI Chat panel
Ctrl+5 / Cmd+5Open Addons panel
Ctrl+B / Cmd+BToggle scope panel (left sidebar)
Ctrl+, / Cmd+,Open Settings modal
Ctrl+P / Cmd+PToggle proxy start/stop (only if Shift is not held)
Ctrl+K / Cmd+KToggle command palette
Ctrl+F / Cmd+FFocus and select the search input
Ctrl+Shift+K / Cmd+Shift+KToggle session comparison view
/Focus search input (when not in an input field)
EscapeClose the currently open panel, palette, or overlay

Note: there is no Ctrl+2 shortcut — the sequence skips from 1 to 3.

On macOS (and to a lesser extent Windows), Ghost registers handlers for Tauri’s native menu bar. This means actions like “File > Export as HAR” or “Tools > Addons” work through the operating system’s menu system, not just through the in-app toolbar.

Menu actions handled:

Menu CategoryActions
FileNew Session, Export (HAR/JSON/CSV/Postman), Import HAR/JSON, Clear Flows
ViewComposer, AI Chat, Map Rules, Addons, Breakpoints, Toggle Theme
ToolsAddons, Breakpoints
TestingBug Reports, Test Scenarios, Generate Bug Report
ProxyToggle Proxy
CertificatesInstall CA (macOS), iOS Device/Simulator setup, Android Device/Emulator setup, Export CA, Cert Status
HelpAbout Ghost

The first time Ghost launches, it shows a setup wizard instead of the main application. The wizard guides the user through two essential steps:

  1. Install Root Certificate — installs Ghost’s CA certificate so it can decrypt HTTPS traffic
  2. Enable System Proxy — configures the operating system to route traffic through Ghost

The wizard offers two paths:

  • “Set Up Ghost” — runs both steps automatically. If the certificate installation fails, it’s marked as skipped but setup continues (the user gets a warning that HTTPS traffic won’t be decrypted). The proxy is then enabled via the completeSetup API with proxy_method: 'system_proxy'.
  • “Skip — I’ll configure manually in Settings” — skips the wizard entirely and lets the user configure everything themselves later.

Once complete, clicking “Launch Ghost” transitions to the main application.

The entire main content area is wrapped in a DropZone component that accepts file drops. When you drag a .har or .json file over the app, a translucent overlay appears with an upload icon, “Drop to import session”, and “Supports .har and .json files.”

On drop:

  1. Creates a new session named <filename> (imported)
  2. Probes the first 1KB of the file to detect if it’s a HAR file (looks for "log" and "entries" keys in the JSON structure)
  3. Imports via the appropriate API endpoint (HAR import or JSON import)
  4. Shows a success toast with the import count and a “Compare” action button that opens session comparison

The maximum import file size is 256MB.

The filter strip sits between the command bar and the flow list, providing quick access to common filters and a visual query builder.

Row 1 — Quick access (always visible):

Smart presets (one-click filters):

  • API Only — shows only JSON/XML/GraphQL/protobuf responses (content_type:json)
  • Errors — shows only error responses (status:>399)
  • Slow — shows only slow responses (duration:>1000)
  • WebSocket — shows only WebSocket connections (has:websocket)
  • Has Interaction — shows flows with browser interactions (has:interaction)

Content type pills (dynamically populated from current traffic): All, API, Pages, JS, CSS, Images, Fonts, Media, Other

Right-side controls: active filter count badge with clear button, save preset button, filter builder toggle.

Row 2 — Filter Builder (toggleable):

A visual query builder with condition rows. Each row has:

  • Field selector: Method, Status, Host, Path, Content Type, Duration (ms), Size (bytes), Tag
  • Operator selector (varies by field): is, :, :>, :<, :>=, :<=
  • Value input: enum dropdown for Method and Content Type, text or number input for others
  • Remove button

Saved filter presets are persisted to localStorage under the key ghost-filter-presets.

Error Boundary: The entire application is wrapped in a React error boundary (class component). When an unhandled exception occurs:

  • The error is logged to the console with the component stack trace
  • The error is sent to Sentry via captureError (if Sentry is configured)
  • A crash recovery screen appears with the Ghost logo (30% opacity), “Something went wrong”, the error message in a <pre> block, and a “Reload” button that calls window.location.reload()

Toast notifications: API errors from store actions are automatically surfaced as toast notifications via the toast bridge. The bridge subscribes to the .error field on 5 stores and fires toast.error() when errors appear. Deduplication prevents the same error from spamming multiple toasts (5-second cooldown, 100-entry dedup cache).

Sentry: In production, Sentry captures unhandled exceptions with:

  • Release tagged as ghost@<version>
  • 10% trace sampling rate
  • Browser tracing integration for performance monitoring
  • Token stripping in breadcrumbs (removes token= from URLs before sending to Sentry)
  • Skipped entirely in development mode

The following UI state is persisted to localStorage across sessions:

KeyWhat It Stores
ghost-app-modeQA or Security mode selection
ghost-themeDark or Light theme
ghost-pinned-hostsPinned domains in the scope panel
ghost-pinned-appsPinned apps in the scope panel
ghost-pinned-devicesPinned devices in the scope panel
ghost-detail-layoutInspector layout (stacked or side-by-side)
ghost-panel-layoutPanel orientation (horizontal or vertical)
ghost-filter-presetsSaved filter presets
ghost-security-modeSecurity scan mode (passive/active-safe/active-full)
ghost-scan-modeAlternative scan mode key
ghost-security-sidebar-tabActive tab in security sidebar (traffic/devices/findings)