Skip to content
Back to Blog

The macOS Sandbox Signing Saga (or Why v1.4.0 Through v1.4.5 Exist)

5 min read

If you looked at our changelog today and noticed six patch releases in a single day, you might be wondering what happened. The short answer: macOS code signing and App Sandbox nearly broke us. Here's the full story.

How MailVault Works Under the Hood

MailVault is a Tauri v2 desktop app. The frontend is React running in a native webview, and the backend is a Node.js Express server compiled into a native binary using Bun. This binary (called a "sidecar") runs alongside the main app and handles all IMAP/SMTP communication.

For macOS distribution, Apple requires apps to be signed with a Developer ID certificate and notarized. We also enable App Sandbox — Apple's security layer that restricts what the app can access on your system. It's required for the Mac App Store and is good practice for any desktop app.

The Problem

After releasing v1.4.0, we discovered that the sidecar server wasn't starting in release builds. The app launched fine, but the backend was silently crashing with exit code 133 (SIGTRAP). Everything worked perfectly in development — only the signed, sandboxed release build was affected.

It took several iterations to peel back the layers:

  • v1.4.0 — Server bound to 0.0.0.0 but the frontend connected to localhost, causing IPv4/IPv6 mismatch. Fixed by binding explicitly to 127.0.0.1.
  • v1.4.1 — The "Add Account" modal used a raw fetch('/api/test-connection') instead of the API helper. In Tauri release builds, the webview origin is tauri://localhost, not http://localhost — so relative URLs resolve to the wrong place.
  • v1.4.2 — Missing com.apple.security.network.server entitlement. The sandbox blocked the sidecar from listening on a port.
  • v1.4.3 — We had shell-based process cleanup (lsof, kill) that's blocked inside App Sandbox. Replaced with Tauri's CommandChild handle.
  • v1.4.4 — The real culprit: Bun-compiled binaries need JIT (Just-In-Time compilation). The JIT compiler allocates executable memory at runtime. While macOS has entitlements to allow this (com.apple.security.cs.allow-jit), Tauri signs all binaries in the app bundle with the same entitlements using --deep. The sidecar ended up with App Sandbox entitlements, which blocked JIT even with the allow-jit flag present.
  • v1.4.5 — Fixed the signing pipeline. The sidecar is now signed separately with its own entitlements (no App Sandbox, JIT allowed). The main app keeps App Sandbox. Signing order matters: sidecar first, then the main app bundle without --deep to preserve the sidecar's entitlements. Also caught that our CI pipeline was uploading Tauri's auto-generated unsigned DMG instead of our properly signed one.

The Fix

The solution has two parts:

  1. Separate entitlements — The sidecar gets entitlements-sidecar.plist (JIT + network, no sandbox). The main app gets entitlements.plist (sandbox + JIT + network).
  2. Correct signing order — Sign the sidecar first with its own entitlements, then sign the main .app bundle without --deep. Using --deep would recursively re-sign everything inside the bundle, overwriting the sidecar's entitlements.

How to Verify the Signing Yourself

If you want to verify that MailVault is properly signed and notarized on your machine, open Terminal and run these commands:

1. Verify the app signature:

codesign --verify --verbose /Applications/MailVault.app

You should see:

/Applications/MailVault.app: valid on disk
/Applications/MailVault.app: satisfies its Designated Requirement

2. Check the main app's entitlements:

codesign -d --entitlements - /Applications/MailVault.app

Look for com.apple.security.app-sandbox set to true — this confirms the app runs in a sandbox.

3. Check the sidecar's entitlements:

codesign -d --entitlements - /Applications/MailVault.app/Contents/MacOS/mailvault-server

The sidecar should have com.apple.security.cs.allow-jit but should not have com.apple.security.app-sandbox.

4. Verify notarization:

spctl --assess --type execute -v /Applications/MailVault.app

You should see:

/Applications/MailVault.app: accepted
source=Notarized Developer ID

5. Check the DMG notarization (before installing):

spctl --assess --type open --context context:primary-signature -v ~/Downloads/MailVault-v1.4.5.dmg

Lessons Learned

  • Test release builds early. Development builds skip code signing and sandboxing entirely. The gap between "works in dev" and "works in release" can be enormous.
  • Sidecars need special treatment. If you're bundling a Bun/Node/Deno compiled binary inside a Tauri app, it needs its own entitlements. Tauri doesn't support per-binary entitlements natively — you have to re-sign after the build.
  • --deep is dangerous. It's convenient but it overwrites all nested signatures with the same entitlements. If different binaries need different entitlements, sign them individually.
  • One build script for local and CI. Having separate signing logic for local builds and CI caused the CI to upload the wrong (unsigned) DMG. Now both use the same script.

If you were affected by the broken releases, we apologize for the inconvenience. v1.4.5 is the stable, properly signed release. Your emails were never at risk — the issue was purely about the app binary not launching its backend server.