> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Offline-First Architecture

> How AppDNA SDKs work without connectivity

# 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)
```

| Priority | Source      | Description                                                                                               | Latency     |
| -------- | ----------- | --------------------------------------------------------------------------------------------------------- | ----------- |
| 1        | **Remote**  | Live fetch from the AppDNA API. Always preferred when network is available.                               | \~100-300ms |
| 2        | **Cached**  | The last successfully fetched config, persisted to local storage on device.                               | \~1ms       |
| 3        | **Bundled** | A static JSON file embedded in the app binary during CI/CD. Used on first launch or when no cache exists. | \~1ms       |

<Info>
  The default config TTL is **1 hour**. 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.
</Info>

***

## Config TTL and Refresh

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

<Steps>
  <Step title="App launches or returns to foreground">
    The SDK checks the age of the cached config against the configured TTL (default: 1 hour).
  </Step>

  <Step title="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`.
  </Step>

  <Step title="Fallback on failure">
    If the remote fetch fails, the SDK continues using the cached config. If no cache exists, the bundled config is used.
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## 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.

<Note>
  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.
</Note>

### Backoff Strategy

When a flush fails, the SDK backs off before retrying. The base delays are `[1, 2, 4]` seconds, each with ±25% random jitter, and the SDK makes at most **3** retries per flush cycle before giving up and waiting for the next cycle:

| Retry | Base delay | With ±25% jitter |
| ----- | ---------- | ---------------- |
| 1     | 1 second   | \~0.75–1.25 s    |
| 2     | 2 seconds  | \~1.5–2.5 s      |
| 3     | 4 seconds  | \~3–5 s          |

After the 3rd retry the flush is abandoned and retried on the next flush cycle. A server-supplied `Retry-After` header, when present, overrides the computed delay. The retry counter resets after a successful flush.

***

## Storage per Platform

Each platform uses native storage mechanisms for maximum reliability:

| Platform         | Config Cache                        | Event Queue                                                            | Anonymous ID                                                                |
| ---------------- | ----------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **iOS**          | UserDefaults                        | JSON file in Application Support (`ai.appdna.sdk/pending_events.json`) | Keychain                                                                    |
| **Android**      | SharedPreferences                   | SQLite database                                                        | Encrypted SharedPreferences                                                 |
| **Flutter**      | Platform channels to native storage | Platform channels to native storage                                    | Platform channels (Keychain on iOS, Encrypted SharedPreferences on Android) |
| **React Native** | Platform channels to native storage | Platform channels to native storage                                    | Platform channels (Keychain on iOS, Encrypted SharedPreferences on Android) |

<Info>
  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.
</Info>

***

## 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.

<Tabs>
  <Tab title="iOS">
    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.

    ```bash theme={null}
    # 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"
    ```
  </Tab>

  <Tab title="Android">
    Place `appdna-config.json` in the `assets/` directory:

    ```
    app/
      src/
        main/
          assets/
            appdna-config.json
    ```

    The SDK reads from `assets/appdna-config.json` on launch automatically.

    ```bash theme={null}
    # Download during CI build
    curl -s -H "x-api-key: $APPDNA_SDK_KEY" \
      https://api.appdna.ai/api/v1/sdk/config-bundle \
      -o app/src/main/assets/appdna-config.json
    ```
  </Tab>

  <Tab title="Flutter">
    Place `appdna-config.json` in the `assets/` folder and declare it in `pubspec.yaml`:

    ```yaml theme={null}
    flutter:
      assets:
        - assets/appdna-config.json
    ```

    ```bash theme={null}
    # Download during CI build
    curl -s -H "x-api-key: $APPDNA_SDK_KEY" \
      https://api.appdna.ai/api/v1/sdk/config-bundle \
      -o assets/appdna-config.json
    ```
  </Tab>

  <Tab title="React Native">
    Place `appdna-config.json` in your project and configure the build to include it:

    **iOS:** Add to Xcode project as a bundle resource.
    **Android:** Place in `android/app/src/main/assets/`.

    ```bash theme={null}
    # Download during CI build
    curl -s -H "x-api-key: $APPDNA_SDK_KEY" \
      https://api.appdna.ai/api/v1/sdk/config-bundle \
      -o appdna-config.json

    # Copy to platform-specific locations
    cp appdna-config.json android/app/src/main/assets/appdna-config.json
    cp appdna-config.json ios/Resources/appdna-config.json
    ```
  </Tab>
</Tabs>

<Warning>
  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.
</Warning>

***

## Offline Experiment Assignment

Experiments work fully offline because assignment is computed locally on-device using a deterministic hash function:

```
bucket = hash(userId, experimentId) % 10000
```

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.

<Note>
  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.
</Note>

***

## Module-Specific Offline Behavior

| Module              | Offline Behavior                                                                                 |
| ------------------- | ------------------------------------------------------------------------------------------------ |
| **Onboarding**      | Flows are presented from cache or bundle. Step completion responses are queued and synced later. |
| **Paywall**         | Paywalls render from cache or bundle. In-app purchases require connectivity to complete.         |
| **Push**            | Push tokens are cached locally. Permission requests work offline. Token registration is queued.  |
| **Billing**         | Entitlements are served from cache. Purchase and restore operations require internet.            |
| **Tracking**        | All events are queued to disk and flushed when connectivity returns.                             |
| **Remote Config**   | Values are served from cache or bundle. Changes arrive on next successful sync.                  |
| **Experiments**     | Variants are assigned offline using MurmurHash3. Exposures are queued.                           |
| **In-App Messages** | Messages are presented from cached rules. Impression events are queued.                          |
| **Surveys**         | Surveys are presented from cache. Responses are queued and synced later.                         |
