The converter ran fine. So why is your Safari extension broken?
xcrun safari-web-extension-converteris honest about exactly one thing: packaging. It wraps your extension in an Xcode project that builds. It makes no promises that the extension inside works, and its warnings cover only a fraction of what differs. Here’s where converted extensions actually break, in the order I usually find them.
1. APIs that exist but do nothing
Safari implements the WebExtension namespace broadly enough that feature detection often passes — the property is there — while the behavior is missing or different. Your code takes the happy path and silently no-ops. Don’t trust typeof browser.x !== 'undefined'; test the actual behavior of every API you depend on, in the oldest Safari you plan to support.
2. Background script assumptions
Safari suspends background scripts aggressively. In-memory state,setIntervalloops, module-level variables holding session data — all of it evaporates when the script is torn down between events. Symptoms look like flakiness: works right after install, breaks “randomly” later. The fix is event-driven structure with state in storage, the same discipline Chrome’s MV3 service workers require.
3. Blocking webRequest, quietly gone
If any part of your extension blocks or modifies requests, that layer doesn’t function in Safari — request interception is declarative-only. This deserves its own migration plan; I wrote one here.
4. Manifest keys Safari ignores
The converter carries your manifest over mostly untouched and warns about some unsupported keys — not all. Behavior tied to ignored keys just doesn’t happen. Commands, some background configurations, and Chrome-specific keys are frequent culprits. Audit the manifest key by key against what Safari documents, not against what the converter accepted.
5. Storage and messaging edge cases
Storage quotas and behavior differ from Chrome, and messaging between content scripts, background, and popup has timing differences — especially around the background script waking up. Code that implicitly relied on Chrome’s timing works in your demo and fails for users. Add explicit retries or handshakes around the first message after idle.
6. It works in Xcode and nowhere else
The build-and-run flow signs with your development identity. The moment someone else needs a build — a teammate, a tester, TestFlight — you’re into provisioning profiles, entitlements, and distribution signing. This isn’t an extension problem; it’s Apple platform work, and it’s the single most common place JavaScript teams stall. Set up CI with proper signing in week one and the whole class of problem disappears.
How to debug systematically
- Enable the develop menu in Safari and inspect the background context and popup directly — the consoles are there, just hidden.
- Write a one-page inventory of every WebExtension API you call. Check each against Safari’s documented support and test the three or four your product actually depends on.
- Assume the background script dies constantly. If behavior improves with the inspector open (which keeps it alive), you’ve found your bug class.
- Test on iOS hardware, not just the simulator.
Stuck on a converted extension that won't behave?
Send me your extension's store link or repo. The port assessment catalogs exactly what's broken and what it takes to fix — one week, fixed price, and the fee credits toward the port.