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.
src/main.ts + src/main/*
- ipc-handlers
- transcription-service
- ai / ask-service
- text-injector
- shortcut-manager
- config-store
- history / dictionary
src/overlay/*
- recording UI
- pcm-audio-recorder
- waveform animation
- sound effects
- app-store (zustand)
src/app/*
- Dashboard page
- History page
- Dictionary page
- Settings page
- hotkey editor
native/global-key-listener
- Rust + rdev grab
- JSON over stdio
- hotkey matching
- event blocking
- 10s heartbeat
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
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:
| Pattern | Direction | Example |
|---|---|---|
| invoke / handle | request → reply | Renderer asks the main process for data and awaits a result — settings:get, history:list, hotkey:set. |
| send / on | renderer → main | Fire-and-forget events — realtime:audio-chunk, realtime:stop, settings:set. |
| webContents.send | main → renderer | Push updates to a window — status:update, transcription:result, history:updated. |
The full catalog is in the IPC channels reference.
What happens on launch
- 01Load config & reconcile shortcutsThe config store is read; if the toggle and hold shortcuts collide, the hold shortcut is reset to a safe fallback.
- 02Create the overlay and register IPCThe overlay window is created and positioned, and all IPC handlers are registered.
- 03Start the native key listenerThe Rust binary is spawned and the two shortcuts are registered with it.
- 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.