macOS setup
macOS widgets require a "sidecar" Xcode project — a small project alongside your Tauri app that compiles the Widget Extension (.appex). After init-macos, a normal pnpm tauri build produces an .app (and DMG) with Contents/PlugIns/*.appex already inside.
Important: On macOS, WidgetKit picks up extension widgets from the installed app bundle.
Addable widgets appear only after you build the app and move the resulting.appto/Applications(or install from the DMG).
Requirements: xcodegen (brew install xcodegen)
Step 1: Initialize the widget project (automatic)
# Recommended: use the CLI (auto-detects bundle ID from tauri.conf.json)
npx tauri-plugin-widgets-api init-macos
# Or with explicit identifiers:
npx tauri-plugin-widgets-api init-macos \
--bundle-id com.example.myapp.widgetkit \
--app-group group.com.example.myappThis creates src-tauri/macos-widget/ with:
Sources/MyWidget.swift— widget code usingTauriWidgetProviderTauriWidgetExtension.entitlements— App Group for the extensionApp.entitlements— App Group for the host app (wired viabundle.macOS.entitlements)project.yml— xcodegen specbuild-widget.sh— builds and signs the.appex(beforeBundleCommand)embed-widget.sh— deprecated emergency re-embed; not part of the normal DX
Step 2: Plugin config (plugins.widgets)
init-macos patches bundle / beforeBundle paths, but does not write plugins.widgets. Without this block the host app fails at plugin init even before you care about WidgetKit:
{
"plugins": {
"widgets": {
"appGroup": "group.com.example.myapp",
"transport": "appGroup",
"extensionBundleId": "com.example.myapp.widgetkit"
}
}
}| Situation | transport |
|---|---|
| Release / Team ID + App Groups enabled | appGroup |
| Mac App Store | appGroup |
| Local ad-hoc signing (no shared App Group container) | widgetContainer |
| Not sure yet | auto once at startup (dev only) — read the log, then pin the winner in conf |
Wrong transport / missing appGroup fails plugin init with a concrete message. Full driver table: Apple data transport (also summarized below).
Step 3: Configure tauri.conf.json bundle hooks
The CLI patches these fields (paths relative to src-tauri/):
{
"build": {
"beforeBundleCommand": "./src-tauri/macos-widget/build-widget.sh"
},
"bundle": {
"macOS": {
"entitlements": "./macos-widget/App.entitlements",
"files": {
"PlugIns/TauriWidgetExtension.appex": "./macos-widget/build/Build/Products/Release/TauriWidgetExtension.appex"
}
}
}
}beforeBundleCommandbuilds and signs the.appex(failures are not swallowed)bundle.macOS.filescopies the.appexintoContents/PlugIns/during bundling- Tauri nested-codesigns
PlugIns/and can produce a normal DMG viabundle.targets
Step 4: Build
pnpm tauri build
# Optional: identity for signing the .appex before bundling
WIDGET_SIGN_IDENTITY="Apple Development: you@example.com (TEAMID)" pnpm tauri buildPipeline:
- Rust + frontend build
build-widget.sh→ signed.appex- Tauri copies PlugIns, nested-codesigns, notarizes (if configured), writes
.app/ DMG
Tip:
{ "scripts": { "build:macos": "tauri build" } }thenpnpm build:macos.
Apple data transport (reference)
See the dedicated guide: Apple data transport.
transport | Host write path | Requirements |
|---|---|---|
appGroup | containerURL(group)/widget_data.json | Real Team ID + App Groups on App + Extension |
userDefaults | App Group UserDefaults suite | Same as appGroup |
widgetContainer | ~/Library/Containers/<appex>/Data/widget_data.json | macOS host not sandboxed; works with ad-hoc |
auto | One-shot probe, then latch | Development only — never ship this |
Override without editing conf: WIDGET_TRANSPORT=widgetContainer.
The widget extension still reads all channels and picks the freshest map. Host-side writes use only the configured driver. Render receipts feed getWidgetDiagnostics, not transport selection.
setItems skips disk I/O when the value is unchanged (no nonce bump).
Match Swift TauriWidgetProvider widgetId with JS setWidgetConfig (CLI templates default to "default").
Code Signing
build-widget.sh signs the .appex before bundling. Identity resolution:
WIDGET_SIGN_IDENTITYAPPLE_SIGNING_IDENTITY- Fallback:
-(ad-hoc)
security find-identity -v -p codesigning| Signing | Widget visible | Recommended transport | Distribution |
|---|---|---|---|
Ad-hoc (-) | Yes | widgetContainer | Local only |
| Apple Development | Yes | appGroup | Local + TestFlight |
| Developer ID | Yes | appGroup | Direct distribution |
Important: The main app's App.entitlements should not include com.apple.security.app-sandbox when using widgetContainer (host must write into the extension container). The widget extension is always sandboxed (required by WidgetKit).
Debugging the widget separately
cd src-tauri/macos-widget
xcodegen generate
open TauriWidgetExtension.xcodeprojSelect the widget scheme in Xcode, set your app as the Host Application, and run with breakpoints.
When it fails
Widget shows "No configuration"
- Confirm
plugins.widgets.appGroupis set and matches Xcode App Groups + JSgroup. - Call
setWidgetConfig(...)from your app before adding the widget. - Verify the app is signed with a real certificate (
security find-identity -v -p codesigning), not ad-hoc — or usetransport: "widgetContainer"for ad-hoc. - Check that the app is not sandboxed (
App.entitlementsshould not containcom.apple.security.app-sandbox) when usingwidgetContainer. - Verify the widget's container has the data file:bash
ls ~/Library/Containers/<your-bundle-id>.widgetkit/Data/widget_data.json - Check widget logs:
log show --last 1m --predicate 'subsystem == "com.tauri.widgets"' --style compact - Ensure
TauriWidgetExtension.entitlementscontainscom.apple.security.app-sandboxand the correct App Group. - Align Swift
widgetIdwith JS (default"default").
Widget doesn't update
- Apple limits widget refreshes to ~40–70 per day.
- Check plugin throttle on iOS (
TAURI_WIDGET_MIN_RELOAD_SECS; Debug0, Release900). On the macOS app host this env is unused — inspectApplyOutcome.reloadfromsetWidgetConfig. - Prefer
{ type: "timer" }for live UI.
More symptoms: Troubleshooting index. Transport: Apple data transport.
