This guide covers the implementation of App Group & Keychain Access for the Futurae iOS SDK, designed to help you avoid common pitfalls and resolve configuration mismatches.
Overview
By default, the Futurae SDK stores sensitive data in the app’s default keychain access group. However, app extensions (like a Notification Service Extension) operate in a separate process and cannot access this data unless App Groups and Keychain Access Groups are explicitly aligned.
When to Use:
- App Extensions: Needed if you use certain optional SDK features (decryptExtraInfo (Encrypted Push Notifications) or getNotificationData (Custom In-App Messaging). Both must be enabled for your Futurae Service by Futurae support before use.) from within an app extension such as a UNNotificationServiceExtension.
- Cross-App Sharing: To share a single Futurae enrollment across multiple apps under the same Team ID.
Part 1 - Implementation Guide
Xcode Configuration:
You must enable these capabilities for both the Main App and Extension targets, via each target's Signing & Capabilities tab (select the target → Signing & Capabilities → + Capability):
- App Groups: Add a matching group identifier (e.g., group.com.example.futurae).
This corresponds to the appGroup used in SDK Initialization below (shown there as {AppGroupId}).
- Keychain Sharing: Add a keychain group (e.g., $(AppIdentifierPrefix)com.example.appPrivateGroup)
This corresponds to the accessGroup used in SDK Initialization below, where $(AppIdentifierPrefix) resolves to your literal Team ID (e.g., {teamID}.com.example.appPrivateGroup).
SDK Initialization:
When launching the SDK, ensure the FTRConfig includes both the appGroup and keychain parameters.
let lockConfiguration = LockConfiguration(type: .none, unlockDuration: 60, invalidatedByBiometricsChange: true)
// CRITICAL: The Keychain Access Group string must include the Team ID prefix in code.
let keychainConfig = FTRKeychainConfig(accessGroup: "{teamID}.com.example.appPrivateGroup")
let config = FTRConfig(
sdkId: "{FuturaeSdkId}",
sdkKey: "{FuturaeSdkKey}",
baseUrl: "https://api.futurae.com:443",
keychain: keychainConfig,
lockConfiguration: lockConfiguration,
appGroup: "{AppGroupId}"
)
try FTRClient.launch(config: config)Migration for Existing Users:
If your app is already in production without these settings, do not simply change the launch configuration, as this results in an invalid state.
Instead, perform a migration:
1. Launch the SDK with the old configuration.
2. Call FTRClient.shared.updateSDKConfig to move data to the new App Group/Keychain.
3. On success, set a flag (e.g., in UserDefaults) so that subsequent launches use the new
Configuration.
Warning: Changing the appGroup and keychain values in the launch configuration without first calling updateSDKConfig will result in an invalid SDK state, requiring an SDK Reset (see Last-Resort Troubleshooting below).
Checking for Existing SDK Data (checkDataExists):
FTRClient.checkDataExists(forAppGroup:keychainConfig:lockConfiguration:) can be called before launching the SDK to check whether SDK data already exists for a specific configuration. It accepts an App Group ID, a FTRKeychainConfig (Keychain Access Group ID), and a LockConfiguration, and returns true only if it finds data matching that exact combination. appGroup and keychainConfig can both be nil to check for data stored without either setting.
let keychainConfig = FTRKeychainConfig(accessGroup: "{KeychainAccessGroupId}")
let lockConfiguration = LockConfiguration(type: .biometricsOnly,
unlockDuration: 60,
invalidatedByBiometricsChange: true)
let sdkDataExists = FTRClient.checkDataExists(forAppGroup: "{AppGroupId}",
keychainConfig: keychainConfig,
lockConfiguration: lockConfiguration)When to use: If the app loses track of which configuration was last used, e.g. after switching to an App Group, changing the keychain access group, or changing lock type, call checkDataExists for each candidate configuration before launch(), and launch with whichever one returns true.
The SDK Reset procedure later in this guide also relies on checkDataExists to confirm both the old and new configurations are fully clean before re-initializing.
Note: On a fresh install (before the SDK has ever launched), checkDataExists will return false for every configuration you try, this is expected, since no database file or keychain data exists yet to match against.
Part 2 - Troubleshooting
Common Pitfalls:
| Pitfall | Impact | Required Action |
| Missing Team ID Prefix | Keychain reads fail at runtime | Use the literal Team ID string in the accessGroup parameter of FTRKeychainConfig, wherever you initialize FTRConfig, typically your app delegate for the Main App, and your UNNotificationServiceExtension subclass (e.g., NotificationService.swift) for the extension. Don't use the $(AppIdentifierPrefix) build variable there (e.g., TeamIDPrefix.KeychainGroup instead of $(AppIdentifierPrefix).KeychainGroup), it only resolves inside Xcode entitlements, not in code. |
| Identifier Mismatches | NSE looks in the wrong container. |
Verify that strings in your constants file exactly match the Xcode "Signing & Capabilities" tab, watch out for text wrapping errors.
|
| Config Mismatch | (Main App vs NSE) Extension cannot access keys / SDK throws a ConfigChanged error. |
Keep the FTRConfig identical between the Main App and NSE. The SDK enforces this for type, invalidatedByBiometricsChange, appGroup, and keychain accessGroup/accessibility |
| Entitlement Scope | Data isolated in one target only, or the SDK throws an error if it can't store items in the specified access group. | Confirm "Keychain Sharing" and "App Groups" list the exact same access group under both the Main App and extension targets entitlements. The SDK throws at launch if the app isn't actually entitled to that group. |
| Double SDK Launch in Extension | A reused NSE process calling launch(config:) a second time will throw/fail. | Check FTRClient.sdkIsLaunched before launching (if FTRClient.sdkIsLaunched { return }). |
Error Reference:
If the launch configuration does not match the persisted state, the SDK will throw specific errors.
To ensure clarity, the following table maps each ConfigChanged error code to its required Resolution Logic. Using the wrong method (e.g., trying to use switchToLockConfiguration for an App Group mismatch) will result in a continued invalid SDK state.
Mappings: Error Codes to Resolution Logic
| Error Code | Root Cause | Required Resolution Logic |
| .configChangedAppGroup | The App Group identifier in FTRConfig does not match the stored data. | Logic A: Call updateSDKConfig. |
| .configChangedKeychainAccessGroup | The Keychain Access Group in FTRConfig does not match the stored data. |
Logic A: Call updateSDKConfig.
|
| .configChangedKeychainItemsAccessibility | The keychain item security level (accessibility) has been changed. |
Logic A: Call updateSDKConfig.
|
| .configChangedLockType | The lock type (e.g., .none vs .biometricsOnly) has been changed. | Logic B: Call switchToLockConfiguration. |
| .configChangedInvalidatedByBiometrics | The flag for invalidation on biometric change has been modified. |
Logic B: Call switchToLockConfiguration.
|
Note: The first three error codes relate to App Group/Keychain (Logic A); the last two relate to Lock Configuration (Logic B). Included here because they share the same ConfigChanged mechanism, not because they're an App Group/Keychain concern.
Catching these errors:
Since launch(config:) throws, wrap it in a do/catch block and inspect the error’s sdkCode to decide which logic to run:
do {
try FTRClient.launch(config: config)
} catch let error as SDKError {
switch error.sdkCode {
case .configChangedAppGroup, .configChangedKeychainAccessGroup, .configChangedKeychainItemsAccessibility:
// Logic A: call updateSDKConfig
case .configChangedLockType, .configChangedInvalidatedByBiometrics:
// Logic B: call switchToLockConfiguration
default:
break
}
}Resolving App Group/Keychain Mismatches (updateSDKConfig) - Logic A:
This logic is for storage and access changes.
Warning — add the App Group to every target that launches the SDK.
Passing an appGroup that the running target is not entitled to is not a recoverable error: the SDK cannot reach the shared container, treats the launch as a first-time install, and clears previously stored SDK data.
A target that carries the correct App Group string in code but is missing the App Group entitlement (typo in the entitlements file, capability not added to the extension, provisioning profile not regenerated) is treated as a fresh install and can delete the shared keychain items the main app depends on. An extension is the likely offender, being the target most often forgotten when capabilities are added.
- Context: This is triggered when moving from a "no-group" setup to an App Group/Shared Keychain setup, or changing those identifiers.
- Implementation:
- Initialize the SDK with the original matching configuration.
- Unlock the SDK first. If your LockConfiguration uses biometrics or an SDK PIN, updateSDKConfig fails with SDKLockError (sdkCode == .locked) while the SDK is locked. Integrations using LockConfigurationType.none are unaffected.
- Call FTRClient.shared.updateSDKConfig(appGroup: "new.group", keychainConfig: newConfig).
- Important: Never call this from an app extension; it must be executed in the Main App to prevent an invalid state.
- Note: updateSDKConfig is asynchronous; it takes success/failure closures (or returns an `AsyncTask` for await).
- After Update: Once successful, all future launches should use the new configuration directly.
Resolving Lock Configuration Mismatches (switchToLockConfiguration) - Logic B:
Note: This is a Lock Configuration concern, not App Group/Keychain, it's included here because it surfaces through the same ConfigChanged mechanism when Main App and NSE configs diverge.
This logic is for lock changes.
Warning — Only migrate when there is something to migrate. A user who has never enrolled has no stored configuration, so launching directly with the new configuration succeeds and no migration is needed.
- Context: Used when you want to change how the user unlocks the SDK (e.g., adding Biometrics) without changing where the data is stored.
- Implementation:
- Launch the SDK with the current configuration that matches the internal state.
- Call switchToLockConfiguration(_:), passing one of the SwitchLockParameters subclasses matching your target lock type (SwitchToLockNone, SwitchToLockBiometrics, SwitchToLockBiometricsOrPasscode, SwitchToLockSDKPin) not a raw LockConfiguration.
- After Update: On the next app launch, initialize the SDK directly with the new configuration.
Last-Resort Troubleshooting
If none of the steps above resolved the issue, work through these in order, they're more invasive (including a full data wipe via SDK Reset) but will get you to a clean, working state.
Rule Out Stale Data:
Perform a Clean Build Folder and Erase All Content and Settings on the simulator to ensure you aren't testing against cached, incorrect configurations.
Validate the Keychain Directly:
Use this to isolate whether the problem is the SDK configuration or the underlying iOS entitlements. Create a throwaway test target with no Futurae SDK dependency, give it the same App Groups and Keychain Sharing entitlements as your real app, then read/write the shared group directly with Apple's APIs. If this succeeds, your entitlements and provisioning are correct and the issue is in how the SDK is configured; if it fails, the problem is at the Xcode/entitlements level, not the SDK.
When testing, watch for Security framework error codes such as errSecMissingEntitlement (-34018). This specific code points directly to an unprovisioned access group or a provisioning profile issue.
Perform an SDK Reset:
If FTRClient.checkDataExists returns false for both the old configuration (nil groups) and your new shared configuration, the SDK is in a clean state.
However, if you are stuck in an error loop despite following Logic A or B, use the SDK Reset to wipe all storage locations and start fresh.
Note: reset can be called either before or after launching the SDK, and the values passed to it should match those used for the SDK launch.
- Reset the old container: FTRClient.reset(appGroup: nil, keychain: nil, ...).
- Reset the App Group container: FTRClient.reset(appGroup: "your.group", keychain: sharedConfig, ...).
- Re-initialize directly with your target configuration.
Note: lockConfiguration has no default value on reset(appGroup:keychain:lockConfiguration:); it must be passed explicitly in both steps above.
Important: SDK reset removes all enrolled accounts, along with cryptographic keys/secrets and the lock configuration. After a reset, the user must re-enroll their account(s).
Futurae SDK Guides:
Configuring App Group and Keychain Access Group
Check if SDK data exists for a specified configuration
Need help?
For any technical support, do not hesitate to contact support@futurae.com.
Comments
0 comments
Article is closed for comments.