Mac App Store Compatibility Changes
This document outlines the changes required to make the Transcribe.dev desktop app compatible with Mac App Store distribution.
Current Distribution Strategy
Direct Distribution via Notarized DMG (like Wispr Flow)
The app is currently distributed directly via download, which allows us to use:
uiohook-napifor global keyboard hooks (hold-to-record)- Full accessibility features without sandbox restrictions
Why App Store Matters
- Discovery - Users can find the app in the App Store
- Trust - Apple review provides credibility
- Updates - Automatic updates via App Store
- Payment - Simplified subscription management
- Enterprise - Easier MDM deployment
Technical Blockers
1. Global Keyboard Hooks (uiohook-napi)
Current Implementation:
- Uses
uiohook-napinpm package - Relies on IOKit HID APIs for low-level keyboard monitoring
- Enables "hold Right Command to record" functionality
Problem:
- IOKit HID APIs are blocked by App Store sandbox
com.apple.security.app-sandboxentitlement is required for App Store- Apps cannot use
hid-controlsandbox exception
Impact:
- Cannot detect keydown/keyup events globally
- Hold-to-record feature would not work
2. Text Injection via AppleScript
Current Implementation:
- Uses clipboard + AppleScript
keystroke "v" using command down - Located in
apps/desktop/src/main/textInjection.ts
Status: This approach IS sandbox-compatible with proper entitlements:
<key>com.apple.security.automation.apple-events</key>
<true/>Solutions
Solution A: CGEventTap (Recommended for App Store)
Overview:
macOS provides CGEventTap API which CAN work in sandboxed apps with Input Monitoring permission.
Key Differences:
| Feature | uiohook-napi (IOKit HID) | CGEventTap |
|---|---|---|
| Sandbox compatible | No | Yes |
| Permission required | Accessibility | Input Monitoring |
| Hold-to-record | Yes | Yes |
| App Store allowed | No | Yes |
Implementation Options:
-
Native Node.js Addon
- Create a native macOS module using N-API
- Use
CGEventTapCreate()from CoreGraphics - Requires Xcode and native compilation
-
Find/Create npm Package
- Search for existing CGEventTap-based packages
- May need to create custom package
-
Electron Native Module
- Use
node-gypto build native addon - Include pre-built binaries for distribution
- Use
Code Example (Native Swift/Objective-C):
import Cocoa
import CoreGraphics
func createEventTap() {
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
guard let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly, // Important: listenOnly for sandbox
eventsOfInterest: CGEventMask(eventMask),
callback: eventCallback,
userInfo: nil
) else {
print("Failed to create event tap")
return
}
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
}
func eventCallback(
proxy: CGEventTapProxy,
type: CGEventType,
event: CGEvent,
refcon: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
if type == .keyDown {
// Handle key down
} else if type == .keyUp {
// Handle key up
}
return Unmanaged.passRetained(event)
}Required Permission:
Users must enable Input Monitoring in:
System Preferences > Privacy & Security > Input Monitoring
Solution B: Toggle Mode (Fallback)
If CGEventTap implementation proves too complex, fall back to toggle mode:
UX Change:
- Current: Hold Right Command to record, release to stop
- New: Press
Cmd+Shift+Dto start, press again to stop
Implementation:
- Use Electron's built-in
globalShortcutmodule - No native code required
- Fully sandbox compatible
Code Example:
import { globalShortcut } from "electron";
let isRecording = false;
globalShortcut.register("CommandOrControl+Shift+D", () => {
isRecording = !isRecording;
if (isRecording) {
startRecording();
} else {
stopRecording();
}
});Required Entitlements
Create apps/desktop/build/entitlements.mac.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- App Store sandbox (REQUIRED) -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Microphone access for voice dictation -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- Network access for Deepgram API -->
<key>com.apple.security.network.client</key>
<true/>
<!-- AppleScript automation for text injection -->
<key>com.apple.security.automation.apple-events</key>
<true/>
<!-- User-selected file access (if saving transcripts) -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>electron-builder Configuration
Update apps/desktop/electron-builder.yml for Mac App Store:
mac:
target:
- target: mas # Mac App Store
arch: [arm64, x64]
- target: dmg # Direct distribution
arch: [arm64, x64]
entitlementsInherit: build/entitlements.mac.plist
mas:
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
provisioningProfile: path/to/embedded.provisionprofile
hardenedRuntime: false # Not needed for MASComparison with Competitors
| App | Distribution | Hold-to-Record | Technology |
|---|---|---|---|
| Wispr Flow | Direct (DMG) | Yes | Electron + IOKit |
| Transcribe.dev (current) | Direct (DMG) | Yes | Electron + uiohook-napi |
| Transcribe.dev (future) | App Store | Yes* | Electron + CGEventTap |
| SuperWhisper | Direct | Yes | Native Swift |
| macOS Dictation | Built-in | Fn key | Native |
*With CGEventTap implementation
Implementation Roadmap
Phase 1: Direct Distribution (Current)
- Use uiohook-napi for hold-to-record
- Notarize app for distribution
- Set up download page on talk.dev
- Implement auto-update (electron-updater)
Phase 2: App Store Preparation
- Research/create CGEventTap npm package
- Implement CGEventTap-based keyboard monitoring
- Test in sandbox environment
- Create App Store entitlements
- Set up App Store Connect account
Phase 3: App Store Submission
- Create App Store screenshots and metadata
- Submit for review
- Address any review feedback
- Launch on App Store
References
- Apple Developer Forums: Accessibility permission in sandboxed app
- Stack Overflow: Input Monitoring API
- Wispr Flow Documentation
- WWDC 2019: Advances in macOS Security
- CGEventTap Documentation
Notes
- Wispr Flow Mac desktop app is distributed directly (not via App Store) and uses Electron
- CGEventTap with
.listenOnlyoption is the key to sandbox compatibility - Input Monitoring permission must be explicitly granted by users
- Consider offering both distribution methods (App Store + direct download)