What Are Entitlements?
Entitlements are key-value pairs that grant your app permission to use specific Apple technologies and services. Each target (main app, widget, App Clip, etc.) can have its own entitlements file.
<!-- Example: widget/generated.entitlements -->
<? xml version = "1.0" encoding = "UTF-8" ?>
<! DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" >
< plist version = "1.0" >
< dict >
< key > com.apple.security.application-groups </ key >
< array >
< string > group.com.example.app </ string >
</ array >
</ dict >
</ plist >
Configuring Entitlements
Via Configuration (Recommended)
Define entitlements in your expo-target.config.js:
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module . exports = {
type: "widget" ,
entitlements: {
"com.apple.security.application-groups" : [ "group.com.example.app" ],
"com.apple.developer.healthkit" : true ,
"com.apple.developer.healthkit.access" : [ "health-records" ],
},
};
This generates a generated.entitlements file automatically.
The generated.entitlements file is managed by the plugin. Don’t edit it directly—update the config instead.
Via Manual File
Place a *.entitlements file in your target directory:
targets/widget/
├── expo-target.config.js
├── Widget.swift
└── widget.entitlements ← Manual entitlements file
The plugin will automatically link it when you run prebuild.
Only one entitlements file is supported per target. If you have both config entitlements and a manual file, the config takes precedence.
Entitlements Schema
The plugin provides full TypeScript types for all Apple entitlements:
type Entitlements = Partial <{
// App Groups - Share data between targets
"com.apple.security.application-groups" : string [];
// HealthKit
"com.apple.developer.healthkit" : boolean ;
"com.apple.developer.healthkit.access" : string [];
// Associated Domains
"com.apple.developer.associated-domains" : string [];
// Apple Pay
"com.apple.developer.in-app-payments" : string [];
// iCloud
"com.apple.developer.icloud-container-identifiers" : string [];
// Sign in with Apple
"com.apple.developer.applesignin" : "Default" [];
// Push Notifications
"com.apple.developer.push-to-talk" : boolean ;
// Network Extensions
"com.apple.developer.networking.networkextension" : (
| "dns-proxy"
| "packet-tunnel-provider"
| "app-proxy-provider"
| "content-filter-provider"
)[];
// And 50+ more...
}>;
See config.ts:10-84 for the complete schema.
App Groups
App Groups enable data sharing between your main app and extensions (widgets, App Clips, share extensions, etc.).
Why App Groups?
Extensions run in separate processes with their own sandboxes. Without App Groups, they cannot share:
UserDefaults data
Files in containers
Keychain items
Core Data databases
Automatic App Groups
Certain target types automatically sync app groups from the main app:
// From target.ts:291-300
export const SHOULD_USE_APP_GROUPS_BY_DEFAULT : Record < ExtensionType , boolean > = {
widget: true , // ✅ Auto-syncs
clip: true , // ✅ Auto-syncs
share: true , // ✅ Auto-syncs
"bg-download" : true , // ✅ Auto-syncs
keyboard: true , // ✅ Auto-syncs
"file-provider" : true , // ✅ Auto-syncs
"watch-widget" : true , // ✅ Auto-syncs
intent: false , // ❌ No auto-sync
// ...
};
If your main app defines app groups and your target type auto-syncs, the plugin copies them automatically.
From with-widget.ts:166-210:
if (
! hasDefinedAppGroupsManually &&
SHOULD_USE_APP_GROUPS_BY_DEFAULT [ props . type ]
) {
const mainAppGroups = config . ios ?. entitlements ?.[ APP_GROUP_KEY ];
if ( Array . isArray ( mainAppGroups ) && mainAppGroups . length > 0 ) {
entitlements [ APP_GROUP_KEY ] = mainAppGroups ;
}
}
Manual App Groups
Override auto-synced app groups or define for non-auto-syncing targets:
// app.json
{
"expo" : {
"ios" : {
"entitlements" : {
"com.apple.security.application-groups" : [
"group.com.example.app"
]
}
}
}
}
// targets/widget/expo-target.config.js
module . exports = ( config ) => ({
type: "widget" ,
entitlements: {
// Explicitly set (overrides auto-sync)
"com.apple.security.application-groups" : [
"group.com.example.app" ,
"group.com.example.widget-specific" ,
],
},
});
App Group Naming
App Groups must:
Start with group.
Use reverse-domain notation
Be unique across the App Store
Recommended patterns:
// Main app bundle ID: com.example.app
// Pattern 1: Use main bundle ID
"group.com.example.app"
// Pattern 2: Add descriptive suffix
"group.com.example.app.shared"
// Pattern 3: Use company domain
"group.com.example.company"
App Clip Entitlements
App Clips have automatic entitlement behavior :
1. Parent Application Identifiers
Automatically set to link the App Clip to the main app:
module . exports = {
type: "clip" ,
// Plugin automatically adds:
// "com.apple.developer.parent-application-identifiers": [
// "$(AppIdentifierPrefix)com.example.app"
// ]
};
From with-widget.ts:86-88:
if ( props . type === "clip" ) {
entitlements [ "com.apple.developer.parent-application-identifiers" ] = [
`$(AppIdentifierPrefix) ${ config . ios ! . bundleIdentifier ! } ` ,
];
}
2. Associated Domains
If not explicitly defined, the plugin attempts to infer from the main app:
// app.json
{
"expo" : {
"ios" : {
"associatedDomains" : [
"applinks:example.com" ,
"webcredentials:example.com"
]
}
}
}
The plugin converts to App Clip format:
// Auto-generated for App Clip:
{
"com.apple.developer.associated-domains" : [
"appclips:example.com"
]
}
From with-widget.ts:119-159:
const sanitizedUrls = associatedDomains
. map (( url ) => {
return url
. replace ( / ^ ( appclips | applinks | webcredentials ) . * :/ , "" )
. replace ( / \/ $ / , "" )
. replace ( / ^ https ? : \/\/ / , "" );
})
. filter ( Boolean );
entitlements [ associatedDomainsKey ] = unique . map (
( url ) => `appclips: ${ url } ` ,
);
App Clips require associated domains to launch from websites. Always define com.apple.developer.associated-domains with the appclips: prefix.
Widgets commonly use:
App Groups (Required for Data Sharing)
module . exports = {
type: "widget" ,
entitlements: {
"com.apple.security.application-groups" : [ "group.com.example.app" ],
},
};
Access shared data in Swift:
let defaults = UserDefaults ( suiteName : "group.com.example.app" )
let value = defaults ? . string ( forKey : "myKey" )
Siri (Optional for App Intents)
entitlements : {
"com.apple.developer.siri" : true ,
}
Share Extension Entitlements
Share extensions need app groups to send data back to the main app:
module . exports = {
type: "share" ,
entitlements: {
"com.apple.security.application-groups" : [ "group.com.example.app" ],
},
};
Common Entitlements
Associated Domains
For universal links, App Clips, and web credentials:
entitlements : {
"com.apple.developer.associated-domains" : [
"applinks:example.com" ,
"appclips:example.com" ,
"webcredentials:example.com" ,
],
}
HealthKit
entitlements : {
"com.apple.developer.healthkit" : true ,
"com.apple.developer.healthkit.access" : [ "health-records" ],
}
HomeKit
entitlements : {
"com.apple.developer.homekit" : true ,
}
iCloud
entitlements : {
"com.apple.developer.icloud-container-identifiers" : [
"iCloud.com.example.app" ,
],
}
Apple Pay
entitlements : {
"com.apple.developer.in-app-payments" : [
"merchant.com.example.app" ,
],
}
Sign in with Apple
entitlements : {
"com.apple.developer.applesignin" : [ "Default" ],
}
Network Extensions
entitlements : {
"com.apple.developer.networking.networkextension" : [
"packet-tunnel-provider" ,
"dns-proxy" ,
],
}
Dynamic Configuration
Use functions to share entitlements between targets:
// targets/widget/expo-target.config.js
module . exports = ( config ) => {
const APP_GROUP = `group. ${ config . ios . bundleIdentifier } ` ;
return {
type: "widget" ,
entitlements: {
"com.apple.security.application-groups" : [ APP_GROUP ],
},
};
};
// targets/share/expo-target.config.js
module . exports = ( config ) => {
// Reuse same app group
const APP_GROUP = `group. ${ config . ios . bundleIdentifier } ` ;
return {
type: "share" ,
entitlements: {
"com.apple.security.application-groups" : [ APP_GROUP ],
},
};
};
Debugging Entitlements
View Generated Entitlements
After prebuild, check:
cat ios/YourApp.xcodeproj/project.pbxproj | grep CODE_SIGN_ENTITLEMENTS
Or open Xcode:
Select target
Signing & Capabilities tab
View capabilities and entitlements
Common Issues
App Groups not syncing between targets
Solution:
Ensure main app defines app groups in app.json
Check target type has appGroupsByDefault: true
Override explicitly if needed in target config
Re-run npx expo prebuild --clean
Provisioning profile errors
Solution:
Open Xcode Signing tab to regenerate profiles
Ensure App Group ID is registered in Apple Developer portal
Check all entitlements are enabled for your app ID
Use Automatic signing initially
App Clip won't launch from website
Solution:
Verify com.apple.developer.associated-domains includes appclips: prefix
Check AASA file is published at /.well-known/apple-app-site-association
Ensure App Clip Experience is configured in App Store Connect
Test with QR code or direct URL first
Next Steps
Code Signing Configure provisioning profiles and certificates
Data Sharing Use app groups to share data between targets