Skip to content

persistent datastore

Joseph Samir edited this page Jun 16, 2026 · 1 revision

Persistent Datastore

The iOS SDK persists all visitor state automatically using platform-native storage. Unlike the JavaScript and PHP SDKs, there is no DataStore interface to implement — the persistence layer is built in and requires no configuration.

What the SDK Persists

Data Storage Implementation
Visitor ID Keychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + UserDefaults mirror KeychainSecureStore + UserDefaultsKeyValueStore
Sticky bucketing decisions, goal deduplication, segments App-private file (ApplicationSupportFileStore via CoordinatedFileStore) Decision store under Application Support/com.convertexperiments.sdk/
Config cache App-private file (atomic write via NSFileCoordinator) CoordinatedFileStore at Application Support/com.convertexperiments.sdk/config-<key>.json
Pending tracking event queue App-private file (atomic write via NSFileCoordinator) CoordinatedFileEventQueueStore at Application Support/com.convertexperiments.sdk/event-queue.json

Visitor ID Persistence

The visitor ID is the foundation of deterministic bucketing. The SDK resolves it at context creation using the following precedence:

  1. Explicit caller-supplied ID — if you pass visitorId to createContext(visitorId:), it is used verbatim with no store access at all.
  2. Keychain — a kSecClassGenericPassword item under the service com.convert.sdk, readable after first unlock post-reboot. The ThisDeviceOnly access constraint keeps the value off iCloud Keychain so it never syncs to other devices.
  3. UserDefaults mirror — a fallback for environments where the Keychain is unavailable (e.g. CI runners without the entitlement). When the mirror contains a value but the Keychain does not, the mirror value is returned AND backfilled into the Keychain so both stores converge.
  4. Fresh UUID — when neither store holds a value, UUID().uuidString is generated, written to both stores, and returned.

This means the same visitor is consistently identified across app launches, app updates, and even app reinstalls (Keychain entries survive reinstall on iOS when the ThisDeviceOnly constraint is used, because the device's Secure Enclave key remains stable).

Note: The SDK does not use IDFA or IDFV for visitor identification. The Keychain UUID is fully under your control and requires no privacy-tracking permission.

// Auto-resolved persistent UUID (no IDFA/IDFV):
let ctx = sdk.createContext()
print(ctx.visitorId)   // e.g. "550E8400-E29B-41D4-A716-446655440000"

// Explicit ID — returned verbatim, Keychain not consulted:
let ctx = sdk.createContext(visitorId: "authenticated-user-id")

Sticky Bucketing Decisions

Once a visitor is bucketed into a variation, that assignment is written to an on-disk file under Application Support. On subsequent runs, the stored decision is read back before any bucketing calculation, so the visitor always sees the same variation even if experience traffic allocation changes.

This happens automatically — no DataStore or cache configuration is needed.

Config Cache

The SDK caches the fetched project configuration to disk (config-<sdkKey>.json in Application Support). On startup, the cached config is loaded first, making the SDK ready immediately even before a live fetch completes. The cache is replaced atomically (via NSFileCoordinator + .atomic write option) so a partial write is never observable.

Event Queue Persistence

Pending tracking events are persisted to event-queue.json in Application Support before the app is backgrounded or terminated. On the next launch, any previously queued events are recovered and delivered. File operations use NSFileCoordinator for forward-compatible cross-process coordination (for future App Group support).

If the event queue file is found corrupted on load, the SDK discards it with a [WARN] log line and starts with an empty queue — it never crashes on bad bytes.

No Custom DataStore Interface

The Android and iOS SDKs handle persistence automatically. Unlike the JavaScript and PHP SDKs, there is no DataStore protocol or interface for you to implement. Persistence is a built-in, zero-configuration capability.

If you need to observe visitor state for your own analytics or debugging, subscribe to the system events the SDK fires:

let token = await sdk.on(.bucketing) { payload in
    // fired when a visitor is bucketed
}
let token = await sdk.on(.conversion) { payload in
    // fired when a conversion is tracked
}

See the Event System guide for the full list of system events.

Visitor ID Continuity

The SDK maintains visitor ID continuity automatically via the Keychain. For authenticated users, you can supply your own stable identifier to ensure cross-device and cross-session consistency:

// Logged-in user: pass your own stable ID
let ctx = sdk.createContext(visitorId: currentUser.id)

// Anonymous user: let the SDK manage the persistent UUID
let ctx = sdk.createContext()

For anonymous-to-authenticated migration (a user who browsed anonymously, then logged in), you manage the transition by creating a new context with the authenticated ID. The two contexts are tracked separately; combining their histories is a product decision outside the SDK's scope.

Storage Locations

All SDK files are stored in the app's private Application Support directory — never in the shared Documents directory, the temp directory, or iCloud Drive. The com.convertexperiments.sdk namespace prevents collisions with other app data.

File Path
Config cache {AppSupport}/com.convertexperiments.sdk/config-{sanitizedKey}.json
Event queue {AppSupport}/com.convertexperiments.sdk/event-queue.json
Decision store {AppSupport}/com.convertexperiments.sdk/ (directory)

Clone this wiki locally