Architecture

System architecture

VoiceFlow is an Electron app split across a main process, two renderer windows, and a native key-listener process. Here's how the pieces fit together.

Runtime

Electron 40 · React 19

Processes

Main + 2 renderers + native

IPC

contextBridge preload

The four moving parts

Like every Electron app, VoiceFlow has a privileged main process and sandboxed renderers. It adds a fourth piece: a standalone Rust binary that listens for global keystrokes. Each has a distinct job.

Main process

src/main.ts + src/main/*

  • ipc-handlers
  • transcription-service
  • ai / ask-service
  • text-injector
  • shortcut-manager
  • config-store
  • history / dictionary
Overlay window

src/overlay/*

  • recording UI
  • pcm-audio-recorder
  • waveform animation
  • sound effects
  • app-store (zustand)
Dashboard window

src/app/*

  • Dashboard page
  • History page
  • Dictionary page
  • Settings page
  • hotkey editor
Native listener

native/global-key-listener

  • Rust + rdev grab
  • JSON over stdio
  • hotkey matching
  • event blocking
  • 10s heartbeat
Process layout. The preload bridge exposes a typed window.electronAPI to both renderers; the native listener speaks JSON over stdio.

Windows & the tray

VoiceFlow is a tray-first app — closing the dashboard doesn't quit it. Two windows exist at all times:

The overlay

A frameless, transparent, always-on-top panel anchored to the bottom center of the active display. It's click-through when idle and hosts the microphone capture.

The dashboard

The normal app window with the Dashboard, History, Dictionary, and Settings screens. Reopened from the tray menu.

Why capture lives in the overlay

Microphone access needs a renderer with a DOM and the Web Audio API. The overlay is always present and visually tied to recording, so it owns the AudioWorklet capture and streams PCM chunks to the main process.

The IPC bridge

Renderers never touch Node directly. A preload script uses contextBridge to expose a single typed window.electronAPI object, and every channel name is a constant shared across processes. Calls fall into three shapes:

PatternDirectionExample
invoke / handlerequest → replyRenderer asks the main process for data and awaits a result — settings:get, history:list, hotkey:set.
send / onrenderer → mainFire-and-forget events — realtime:audio-chunk, realtime:stop, settings:set.
webContents.sendmain → rendererPush updates to a window — status:update, transcription:result, history:updated.

The full catalog is in the IPC channels reference.

What happens on launch

  1. 01Load config & reconcile shortcutsThe config store is read; if the toggle and hold shortcuts collide, the hold shortcut is reset to a safe fallback.
  2. 02Create the overlay and register IPCThe overlay window is created and positioned, and all IPC handlers are registered.
  3. 03Start the native key listenerThe Rust binary is spawned and the two shortcuts are registered with it.
  4. 04Create the tray & check permissionsThe tray menu is built, and on macOS the app checks Accessibility — offering to open System Settings if it's missing.