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-napi for global keyboard hooks (hold-to-record)
  • Full accessibility features without sandbox restrictions

Why App Store Matters

  1. Discovery - Users can find the app in the App Store
  2. Trust - Apple review provides credibility
  3. Updates - Automatic updates via App Store
  4. Payment - Simplified subscription management
  5. Enterprise - Easier MDM deployment

Technical Blockers

1. Global Keyboard Hooks (uiohook-napi)

Current Implementation:

  • Uses uiohook-napi npm 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-sandbox entitlement is required for App Store
  • Apps cannot use hid-control sandbox 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

Overview: macOS provides CGEventTap API which CAN work in sandboxed apps with Input Monitoring permission.

Key Differences:

Featureuiohook-napi (IOKit HID)CGEventTap
Sandbox compatibleNoYes
Permission requiredAccessibilityInput Monitoring
Hold-to-recordYesYes
App Store allowedNoYes

Implementation Options:

  1. Native Node.js Addon

    • Create a native macOS module using N-API
    • Use CGEventTapCreate() from CoreGraphics
    • Requires Xcode and native compilation
  2. Find/Create npm Package

    • Search for existing CGEventTap-based packages
    • May need to create custom package
  3. Electron Native Module

    • Use node-gyp to build native addon
    • Include pre-built binaries for distribution

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+D to start, press again to stop

Implementation:

  • Use Electron's built-in globalShortcut module
  • 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 MAS

Comparison with Competitors

AppDistributionHold-to-RecordTechnology
Wispr FlowDirect (DMG)YesElectron + IOKit
Transcribe.dev (current)Direct (DMG)YesElectron + uiohook-napi
Transcribe.dev (future)App StoreYes*Electron + CGEventTap
SuperWhisperDirectYesNative Swift
macOS DictationBuilt-inFn keyNative

*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

Notes

  • Wispr Flow Mac desktop app is distributed directly (not via App Store) and uses Electron
  • CGEventTap with .listenOnly option is the key to sandbox compatibility
  • Input Monitoring permission must be explicitly granted by users
  • Consider offering both distribution methods (App Store + direct download)

On this page