Safari Extension Development
Safari Extension is technically the most complex browser extension. Apple requires a native Mac/iOS wrapper, signing via Apple Developer Program, and App Store approval. However, it's the only legal way to extend Safari — especially on iOS, where Safari remains the only browser for any WebKit engine.
Architectural options
Safari Web Extension (modern, since Safari 14+) — based on WebExtensions API, similar to Chrome/Firefox. Requires wrapping in a native Mac/iOS app. This is the main recommended format since 2020.
Safari App Extension (deprecated) — native Swift/ObjC code with native extension points. More capabilities (access to native APIs), but significantly more complex.
Content Blocker — simplified format for blocking content via JSON rules only. Doesn't require JavaScript, works on iOS. Used for ad blockers.
Project structure
Safari Web Extension is an Xcode project where the extension is an app target extension:
MyApp.xcodeproj
├── MyApp/ # Main wrapper application
│ ├── AppDelegate.swift
│ └── ContentView.swift # Minimal UI (activation instructions)
└── MyApp Extension/ # The extension itself
├── manifest.json
├── background.js
├── content.js
├── popup.html
└── Resources/
└── icons/
Converting Chrome Extension to Safari
Apple provides the safari-web-extension-converter tool:
# Install Xcode (required) and xcrun
xcrun safari-web-extension-converter \
./my-chrome-extension \
--project-location ./safari-project \
--app-name "My Extension" \
--bundle-identifier com.example.my-extension \
--swift
The converter creates an Xcode project with a native wrapper and copies all extension files. After that, you need to:
- Fix incompatible APIs (list below)
- Configure app icons (requires set of 10+ sizes)
- Sign via Apple Developer account
Manifest V2 vs V3 in Safari
| Version | Safari | Status |
|---|---|---|
| MV2 | Safari 14–17 | Works, deprecated |
| MV3 | Safari 15.4+ | Recommended |
Safari 15.4 added MV3 support. For maximum compatibility with iOS 15+, we use MV3.
Incompatible APIs and workarounds
// chrome.* does NOT work in Safari — use browser.* or polyfill
// webextension-polyfill solves most problems
import browser from 'webextension-polyfill';
// Does NOT work in Safari:
// chrome.scripting.executeScript (MV3) — partial from Safari 16
// chrome.declarativeNetRequest — from Safari 15.4
// chrome.sidePanel — Chrome only
// Works in Safari:
// browser.storage.local / sync
// browser.tabs
// browser.runtime.sendMessage
// browser.webNavigation
// browser.cookies (with permission)
Safari specifics:
-
browser.storage.syncworks via iCloud — syncs between Mac and iPhone/iPad -
Content Security Policy is stricter in Safari — some
eval-dependent libraries don't work - Background Service Worker — Safari sometimes terminates SW aggressively, need to reinitialize state
iOS extensions
Safari on iOS supports extensions starting from iOS 15. This opens a unique market unavailable for Chrome/Firefox extensions:
// SafariExtensionViewController.swift — native code for iOS part
import SafariServices
class SafariExtensionViewController: SFSafariExtensionViewController {
static let shared = SafariExtensionViewController()
// Called when opening popup in Safari on iOS
override func viewDidLoad() {
super.viewDidLoad()
// Embedded WebView with popup.html loads automatically
}
}
// JavaScript → Native Bridge
// From content script, you can call native code via:
browser.runtime.sendNativeMessage('com.example.app', { type: 'DO_NATIVE_THING' });
Signing and publishing
# Build for testing (development signing)
xcodebuild -scheme "MyApp" -configuration Debug \
-destination "platform=macOS" build
# Archive for App Store
xcodebuild -scheme "MyApp" -configuration Release \
-archivePath build/MyApp.xcarchive archive
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build/export \
-exportOptionsPlist ExportOptions.plist
Requirements for publishing:
- Apple Developer Program ($99/year)
- Signing for Mac App + Safari Extension entitlements
- App Review (~1–3 days for updates, up to 7 days for new)
- Minimum main app functionality — App Store won't accept "empty" wrappers
Device testing
# Mac: enable Developer Mode
# Safari → Preferences → Extensions → Show "Develop" menu
# Develop → Allow unsigned extensions
# iOS: Settings → Safari → Extensions (appears after app installation)
Manifest structure for Safari MV3
{
"manifest_version": 3,
"name": "My Safari Extension",
"version": "1.0",
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"run_at": "document_idle"
}],
"action": {
"default_popup": "popup.html",
"default_icon": "icons/toolbar-icon.svg"
},
"permissions": ["storage", "activeTab"],
"browser_specific_settings": {
"safari": {
"strict_min_version": "15.4"
}
}
}
Common issues and solutions
Issue: Service Worker terminates too quickly.
Solution: Don't store state in SW memory, use chrome.storage.session (Safari 16.4+) or reinitialize on each start.
Issue: fetch in Content Script is blocked by CORS.
Solution: Redirect request through Background (browser.runtime.sendMessage), as Background bypasses CORS restrictions from the page.
Issue: Extension doesn't appear on iOS. Solution: App must be installed and launched at least once. Extension is enabled manually in Settings → Safari → Extensions.
Timeline
Converting ready Chrome extension to Safari with fixing incompatibilities and Mac App Store publication — 5–8 working days. Developing cross-platform extension (Mac + iOS) from scratch, including native wrappers, testing, and App Review — 15–25 days. Main time — Xcode setup, Apple Developer bureaucracy, and App Review waiting.







