Multi 3.0: Codesigning, dynamic libraries, and macOS notifications

Kofi Gumbs ·

Multi is a macOS app for creating native wrappers around your favorite websites. Since Multi apps use Apple’s WebKit engine, they are generally less resource-intensive than their Electron equivalents. The Multi runtime also connects web platform APIs to macOS native widgets, like notifications—the little feature that turned the 3.0 release into a complete rewrite.

Prior to this release, Multi used the deprecated NSUserNotification to show banner alerts. These APIs still seem to work OK, but fixing some of the long-standing GitHub issues would have required me to invest further into a sinking ship. I figured migrating to the newer UNNotification might magically solve some of those issues, but even if it didn’t I’d still be in a better position for researching solutions. At a surface level the APIs work similarly, and it was simple to get the new code compiling; but when I went to test my Multi app, no notifications appeared.

Eventually I discovered that UNUserNotificationCenter automatically denies notification permission requests unless the app is codesigned. This restriction was a problem for Multi because codesign requires that an apps bundle contains no references to external artifacts. In the old architecture, creating a Multi app would add a symlink in the place where macOS expected the main executable. That symlink pointed to an executable runtime within Multi itself, which meant that updating Multi automatically updates any wrapper apps. codesign complains (rightfully so) with this setup since the executable artifact lives outside of the signed bundle.

The solution: I now ship the runtime as a dynamic library instead of an executable, and Multi wrapper apps contain a small initializer binary that loads it. codesign is happy because the resulting bundle is statically self-contained. And users are happy because updating Multi still updates behavior in existing wrapper apps. The initializer binary itself should only need to change if I’ve done something tragically wrong in these 10 lines of Swift.