Skip to main content

Offline-First Architecture

AppDNA SDKs are designed to deliver a fully functional experience even when the device has no internet connectivity. Every feature — onboarding flows, paywalls, experiment assignments, event tracking — works offline by default.

Three-Tier Config Priority

When the SDK needs configuration, it resolves it using a strict fallback chain:
Remote (live fetch from API)
        |
        v  unavailable or expired?
Cached (last successful fetch, persisted to disk)
        |
        v  no cache exists?
Bundled (appdna-config.json embedded in app binary at build time)
PrioritySourceDescriptionLatency
1RemoteLive fetch from the AppDNA API. Always preferred when network is available.~100-300ms
2CachedThe last successfully fetched config, persisted to local storage on device.~1ms
3BundledA static JSON file embedded in the app binary during CI/CD. Used on first launch or when no cache exists.~1ms
The default config TTL is 5 minutes. On app launch, the SDK attempts a remote fetch with a 3-second timeout. If the fetch fails or times out, the SDK immediately falls back to cached config. If no cache exists (e.g., first launch with no connectivity), the bundled config is used. Your app is never blocked waiting for a network response.

Config TTL and Refresh

The SDK checks whether the cached config has expired on every app launch and every return from background:
1

App launches or returns to foreground

The SDK checks the age of the cached config against the configured TTL (default: 5 minutes).
2

Remote fetch attempted

If the cache is stale (or does not exist), the SDK makes a background request to GET /api/v1/sdk/config-bundle.
3

Fallback on failure

If the remote fetch fails, the SDK continues using the cached config. If no cache exists, the bundled config is used.
4

Cache updated on success

If the remote fetch succeeds, the new config is written to disk and used immediately. UI components backed by config (paywalls, onboarding) update on the next presentation.

Event Queue

Events are never dropped. The SDK persists every event to disk before attempting delivery:
  1. When track() is called, the event is written to a persistent on-disk queue immediately.
  2. The queue is auto-flushed every 30 seconds or when it reaches 20 events, whichever comes first.
  3. If the flush fails (no connectivity, server error, timeout), events remain in the queue and are retried on the next flush cycle with exponential backoff.
  4. Events are only removed from the queue after the server returns a successful acknowledgment.
Because events are persisted to disk, they survive app restarts, force-quits, and even device reboots. No event is ever lost due to a crash or connectivity issue.

Backoff Strategy

When a flush fails, the SDK backs off before retrying:
AttemptDelay
11 second
25 seconds
330 seconds
42 minutes
5+5 minutes (max)
The backoff resets to zero after a successful flush.

Storage per Platform

Each platform uses native storage mechanisms for maximum reliability:
PlatformConfig CacheEvent QueueAnonymous ID
iOSUserDefaultsUserDefaultsKeychain
AndroidSharedPreferencesSharedPreferencesEncrypted SharedPreferences
FlutterPlatform channels to native storagePlatform channels to native storagePlatform channels (Keychain on iOS, Encrypted SharedPreferences on Android)
React NativePlatform channels to native storagePlatform channels to native storagePlatform channels (Keychain on iOS, Encrypted SharedPreferences on Android)
The anonymous ID is stored in the most secure storage available on each platform (Keychain on iOS, Encrypted SharedPreferences on Android). This ensures the ID survives app reinstalls on iOS (as long as the Keychain entry is preserved) and is protected from unauthorized access.

Config Bundle Embedding

For zero-latency first launch, you can embed a config bundle JSON file in your app binary. The SDK automatically detects this file and uses it as the last-resort fallback when no remote or cached config is available.
Add appdna-config.json to your Xcode project as a bundle resource:
  1. Drag appdna-config.json into your Xcode project navigator.
  2. Ensure it is added to your app target under Build Phases > Copy Bundle Resources.
  3. The SDK automatically looks for this file in the main bundle on launch.
# Download during CI build (Xcode Build Phase or Fastlane)
curl -s -H "x-api-key: $APPDNA_SDK_KEY" \
  https://api.appdna.ai/api/v1/sdk/config-bundle \
  -o "${SRCROOT}/Resources/appdna-config.json"
The embedded bundle is a fallback only. The SDK always prefers a fresher remote or cached config when available. Embed a bundle to ensure your app works correctly on first launch before any network request completes.

Offline Experiment Assignment

Experiments work fully offline because assignment is computed locally on-device using a deterministic hash function:
bucket = MurmurHash3(userId + experimentId + salt) % 100
The same user always gets the same variant for the same experiment, regardless of whether the device has connectivity. All the SDK needs is the experiment definition (which is included in the config bundle) and the user’s ID. Once the config is available — whether from remote, cache, or bundle — the SDK can assign users to experiment variants and return results from getVariant() with zero network dependency.
Exposure events are still queued locally when offline and flushed to the server when connectivity returns. This means your experiment analytics remain accurate even when users interact with experiments while offline.

Module-Specific Offline Behavior

ModuleOffline Behavior
OnboardingFlows are presented from cache or bundle. Step completion responses are queued and synced later.
PaywallPaywalls render from cache or bundle. In-app purchases require connectivity to complete.
PushPush tokens are cached locally. Permission requests work offline. Token registration is queued.
BillingEntitlements are served from cache. Purchase and restore operations require internet.
TrackingAll events are queued to disk and flushed when connectivity returns.
Remote ConfigValues are served from cache or bundle. Changes arrive on next successful sync.
ExperimentsVariants are assigned offline using MurmurHash3. Exposures are queued.
In-App MessagesMessages are presented from cached rules. Impression events are queued.
SurveysSurveys are presented from cache. Responses are queued and synced later.