The Problem
Dictation tools today trade privacy for performance. Cloud-dependent services send audio to remote servers, and "local-first" alternatives often lock users into hosted APIs or lack the polish of commercial products. Professionals who need voice input without surrendering data to third parties have limited options that don't require compromising on accuracy or workflow integration.
What This Does
OpenFlow is a desktop dictation application built on Tauri 2 + Rust + React that runs locally-first by default. The codebase organizes into five distinct projects: src (210 files, 181 code files) contains the React frontend; src-tauri (162 files, 77 code files) holds the Rust backend, audio pipeline, and transcription managers; .nix (6 files) provides NixOS configuration; scripts (8 files) contains build and utility scripts; and skills (8 files) adds app extensions. The audio pipeline and transcription managers originate from the forked Handy project (MIT), and the app supports three independent backend modes for STT and AI cleanup — local on-device models, self-hosted endpoints, or remote providers. API keys for remote providers are stored in the OS keychain, never in plaintext settings files. The entry point main in src-tauri/src/audio_toolkit/bin/cli.rs:176 reaches 98 functions and triggers audio synthesis via hound::WavReader::open, while start in src-tauri/src/actions.rs:52 reaches 208 functions and is the primary execution path from a shortcut event.
How It Is Wired
Execution begins at main in src-tauri/src/audio_toolkit/bin/cli.rs:176, which reaches 98 functions and synthesizes audio through the filesystem via hound::WavReader::open. The start function in src-tauri/src/actions.rs:52 is the most broadly reached entry point (208 functions), itself called from a single place — the shortcut event handler in src-tauri/src/shortcut/handler.rs:29. That handler, handle_shortcut_event, reaches 55 functions and is itself called from 2 places. From start, control flows through start_stream which opens self.router.open for filesystem access. A complete path from entry to outbound effect: close -> stop_recording -> get_settings -> get_default_settings -> default_post_process_provider_id, where the model identifier resolves to "openai".to_string, indicating an outbound network call to a remote STT provider. The call graph shows get_settings is called from 153 places and write_settings from 86 places, making the settings hub the highest-blast-radius module — changes ripple across nearly every component. Five modules participate in circular imports (src/stores/navigationStore.ts, src/components/Sidebar.tsx, src/components/settings/index.ts), which must be broken before safe refactoring. The most connected module is src/hooks/useSettings (Ca=59, Ce=1, instability=0.02), depended on by 59 other modules; keeping it stable and small is the highest-priority maintenance goal. Files with oversized scope — src/bindings.ts (1712 lines), src-tauri/src/actions.rs, and src-tauri/src/managers/agent_run.rs — concentrate wide-ranging effects and should be split into cohesive units.
How To Use It
Setup: Download the latest installer from GitHub Releases or openflow.computer. The app is unsigned; on macOS, unblock Gatekeeper with xattr -dr com.apple.quarantine /Applications/OpenFlow.app after dragging the app to Applications, then grant microphone, accessibility, and input monitoring permissions under System Settings → Privacy & Security. On Windows, run the installer and grant equivalent permissions.
Configuration: Remote provider API keys are stored via the OS keychain. Local model paths and backend mode selections are configured in the app's settings UI. No environment variables or config files are required for basic local operation.
Running it: Launch OpenFlow.app. Press the global shortcut (default configured in settings) to start/stop recording, or enable wake-word hands-free mode from the settings panel. The app will attempt local on-device STT first; if a remote provider is configured, it will use that instead.
Real-World Use
A user enables wake-word mode and sets STT to "local on-device model" while keeping AI cleanup on "self-hosted endpoint." The wake word triggers handle_shortcut_event, which calls start in actions.rs, routing audio through the recorder and into the transcription manager. Transcribed text flows into the focused application via the Tauri clipboard API. After each dictation session, the post-processing pass runs through the self-hosted cleanup endpoint, and usage metrics (words-per-minute, time saved) are computed locally and stored in the embedded SQLite database via src-tauri/src/managers/history.rs. If the user switches to a remote STT provider, the app retrieves the API key from the keychain and routes audio directly to the external endpoint, with all network activity traced through the single model-resolution path.
Code Health & Issues
- HIGH — Pin third-party GitHub Actions to commit SHAs:
anthropics/claude-code-action@beta,dtolnay/rust-toolchain@stable,swatinem/rust-cache@v2,oven-sh/setup-bun@v2are pinned only by tag. A tag move changes which code runs with your CI tokens and secrets. Fix: replace@vNwith 40-character commit SHA and let Dependabot bump SHAs. - MEDIUM — Declare least-privilege permissions for GITHUB_TOKEN: two workflows (
nix-check.ymlamong others) declare no permissions, so the token inherits the repository default and can push commits or mint releases from inside CI. Fix: addpermissions: contents: readat the top of each workflow and widen per job only where needed. - MEDIUM — Review the install lifecycle script:
package.jsondeclares apostinstallscript that fetches a binary from a URL, making the build depend on an unreviewed remote source. Fix: move the work into an explicit build step or setignore-scriptsin CI and run it by name. - MEDIUM — Test coverage is extremely low: 1 test file against 266 source files (ratio 0.004). A green test badge gives a false sense of safety. Fix: add tests for the highest fan-in modules first (e.g.,
src/hooks/useSettingswith 59 dependents). - LOW — Three workflow jobs declare no
timeout-minutes:build.yml,main-build.yml, andrelease.ymlwill default to the six-hour platform timeout, meaning two-hourly schedules can overlap behind a wedged step. Fix: addtimeout-minuteswith a realistic bound to each job.
The Bottom Line
OpenFlow delivers a functional, privacy-oriented dictation experience that runs entirely locally if desired, with the flexibility to plug in self-hosted or remote models. The architecture is sound but shows signs of its Handy fork origin — the settings hub (useSettings) is a high-traffic chokepoint, circular imports need resolution, and several files exceed sustainable size limits. Test coverage is minimal, and CI pinning is loose. It's well-suited for technical users who want on-device dictation without sacrificing the ability to swap in external models, but it requires ongoing maintenance to keep the import graph stable and the CI pipeline secure.