Entitlements & Capabilities
Push, associated domains, keychain, and background modes - configuring iOS entitlements through app.config.ts and config plugins so Expo prebuild writes the correct .entitlements file before EAS Build or TestFlight.
Search across all documentation pages
Push, associated domains, keychain, and background modes - configuring iOS entitlements through app.config.ts and config plugins so Expo prebuild writes the correct .entitlements file before EAS Build or TestFlight.
Quick-reference recipe card - copy-paste ready.
// app.config.ts - common entitlements via Expo config
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "ShopApp",
slug: "shop-app",
ios: {
bundleIdentifier: "com.example.shopapp",
// Universal Links
associatedDomains: ["applinks:shop.example.com"],
// Keychain access group (optional - Secure Store)
entitlements: {
"keychain-access-groups": ["$(AppIdentifierPrefix)com.example.shopapp.shared"],
},
infoPlist: {
UIBackgroundModes: ["remote-notification"],
},
},
plugins: [
"expo-router",
[
"expo-notifications",
{
icon: "./assets/notification-icon.png",
color: "#ffffff",
sounds: ["./assets/notification.wav"],
},
],
],
};
export default config;When to reach for this:
remote-notification for silent push.Step 1 - Plugin + permission strings
// app.config.ts
export default {
expo: {
plugins: ["expo-notifications"],
ios: {
infoPlist: {
UIBackgroundModes: ["remote-notification"],
},
},
},
};Step 2 - Apple Developer portal
.p8) - upload to EAS or your push provider.eas credentials → iOS → Push Key.Step 3 - Rebuild and test on device
npx expo prebuild --platform ios
eas build --profile development --platform ios
# Push does not deliver to simulators - use USB device// app/_layout.tsx (excerpt)
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowList: true,
}),
});ios: {
associatedDomains: [
"applinks:shop.example.com",
"webcredentials:shop.example.com", // Password AutoFill (optional)
],
},apple-app-site-association on the domain - see iOS Universal Links.applinks: - not bare hostnames.xcrun simctl openurl booted "https://shop.example.com/orders/42"ios: {
entitlements: {
"keychain-access-groups": [
"$(AppIdentifierPrefix)com.example.shopapp.shared",
],
"com.apple.security.application-groups": [
"group.com.example.shopapp",
],
},
},import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("refresh_token", token, {
keychainAccessible: SecureStore.WHEN_UNLOCKED,
// accessGroup: "group.com.example.shopapp", // when extension must read same secret
});| Mode | Legitimate use | Config |
|---|---|---|
remote-notification | Silent push wakes app | infoPlist.UIBackgroundModes + push entitlement |
fetch | Legacy background fetch | Plugin + real periodic sync implementation |
processing | BGTaskScheduler deferred work | expo-background-task plugin |
audio | Playback continues backgrounded | expo-av / audio plugin |
location | Turn-by-turn / approved tracking | expo-location + usage strings |
plugins: [
"expo-background-task", // adds processing + BGTaskSchedulerPermittedIdentifiers
],What this demonstrates:
.entitlements at prebuild.Entitlements are key-value pairs in YourApp.entitlements that Apple code-signs into the IPA. They gate APIs:
<!-- Generated excerpt - do not hand-edit under CNG -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:shop.example.com</string>
</array>
<key>aps-environment</key>
<string>development</string> <!-- production in App Store builds -->| Capability | Entitlement key | Set via |
|---|---|---|
| Push | aps-environment | expo-notifications + EAS credentials |
| Universal Links | com.apple.developer.associated-domains | ios.associatedDomains |
| App Groups | com.apple.security.application-groups | ios.entitlements or widget plugin |
| Keychain groups | keychain-access-groups | ios.entitlements |
| Sign in with Apple | com.apple.developer.applesignin | expo-apple-authentication plugin |
| Build type | aps-environment | APNs endpoint |
|---|---|---|
| Debug / dev client | development | api.sandbox.push.apple.com |
| TestFlight / App Store | production | api.push.apple.com |
EAS sets this from the distribution profile. Mismatch symptoms: token registers but notifications never arrive.
ios.entitlements// Prefer high-level config fields when Expo documents them
ios: { associatedDomains: ["applinks:example.com"] }
// Use ios.entitlements for keys without first-class fields
ios: {
entitlements: {
"com.apple.developer.networking.networkextension": ["app-proxy-provider"],
},
}
// Or a local config plugin with withEntitlementsPlistSee Config Plugins - withEntitlementsPlist is the escape hatch.
# After eas build --local or Xcode Archive
unzip -q ShopApp.ipa -d /tmp/shop
codesign -d --entitlements :- /tmp/shop/Payload/ShopApp.app > /tmp/entitlements.plist
/usr/libexec/PlistBuddy -c Print /tmp/entitlements.plistCompare output to app.config.ts before blaming application code.
app.config.ts only.applinks: prefix - associated domain silently ignored. Fix: applinks:host.example.com.eas build.bundleIdentifier.group. identifiers - see App Extensions & Widgets.| Alternative | Use When | Don't Use When |
|---|---|---|
| Xcode Capabilities GUI | One-off brownfield rescue | CNG projects - not durable |
withEntitlementsPlist plugin | Custom enterprise entitlements | Simple push - use expo-notifications |
| Custom scheme only | Internal OAuth | Marketing HTTPS links - use associated domains |
| FCM-only on Android | Cross-platform push backend | iOS still needs APNs entitlements |
ios.infoPlist.NSCameraUsageDescription, etc. - often set automatically by Expo module plugins (expo-camera, expo-location).expo-apple-authentication plugin and entitlement via config.applinks:*.example.com - AASA still requires per-host files.eas build runs prebuild (unless ios/ committed) and signs with credentials matching the App ID capabilities enabled in Apple Developer portal.withEntitlementsPlistprocessing background modeStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026