Skip to main content

Core Concepts

This page covers the foundational architecture and design decisions behind AppDNA. Understanding these concepts will help you integrate the SDK effectively and get the most out of the platform.

SDK Architecture

The AppDNA SDK follows a singleton pattern with module namespaces. After calling configure(), the SDK is accessible through a single global instance. Each feature area is organized as a namespace on that instance: Each namespace operates independently but shares the core event pipeline, identity state, and configuration layer. For a quick reference on what’s configured in the Console vs. what requires code, see Dashboard vs. Code.

Offline-First Design

AppDNA SDKs are built for unreliable networks. The SDK never assumes connectivity is available and degrades gracefully when it is not.

Configuration Priority

The SDK resolves configuration using a three-tier fallback:
The default config TTL is 1 hour. After the TTL expires, the SDK attempts a remote fetch. If the fetch fails, the cached config continues to be used until a successful refresh. If the bootstrap request fails (for example, the app starts offline: the SDK never answers it from the HTTP cache), the SDK runs on cached and bundled config and retries the bootstrap — when the network comes back, on foreground, and after a backoff of up to 5 minutes (at most 10 retries; a 401 or 403 ends them) — then fetches the remote config once a retry succeeds. See Offline-First Architecture.

Event Queue

When the SDK records an event:
  1. The event is written to a persistent on-disk queue (a JSON file in Application Support on iOS, a SQLite database on Android).
  2. The queue is auto-flushed every 30 seconds, when the app goes to the background or returns to the foreground, and when it reaches a batch size set by the network (100 events on Wi-Fi or a wired connection, metered or not; 20 on a cellular connection the system marks as expensive (iOS) or metered (Android), which is every cellular connection on iOS and most cellular plans on Android; 50 on any other cellular connection), capped by the batchSize option when you set one. Each upload sends at most that many events.
  3. If the flush fails (no connectivity, a 5xx, a 408 or a 429), events remain in the queue and are retried.
  4. Events are removed from the queue after a successful upload.
Because events are persisted to disk, they survive app restarts, crashes, force-quits and device reboots. Some events are still dropped: a batch the server rejects with another 4xx, the oldest events beyond 10,000 events or 5 MB, events older than 7 days, and queued events when analytics consent is revoked. See When Events Are Dropped.

Config Bundle

A config bundle is a versioned JSON snapshot generated server-side. It contains all active configuration for your app: onboarding flows, paywalls, in-app messages, experiments, feature flags, and remote config values.

How It Works

  1. The server regenerates the bundle after every publish that changes SDK config (publishes are grouped into fixed 15-second clock windows — :00–:15, :15–:30 and so on, not 15 seconds from the first publish — and each window’s publishes are built together, once, about a second after the window ends, so a new bundle is usually ready within about 20 seconds of a publish), from the same documents the SDK reads live. Regenerate under Settings → SDK → Config Bundle in the Console, or POST /api/v1/sdk/config-bundle/generate, builds one at once.
  2. Your build downloads it with your SDK key (GET /api/v1/sdk/config-bundle) and embeds it in the app binary as appdna-config.json. See Config Bundles in CI/CD.
  3. The SDK never downloads bundles. It reads the embedded file once its bootstrap request has succeeded or failed, and uses it for the sections it has no cached config for, typically on a first launch without network. A remote fetch from Firestore replaces it.

Embedding a Bundle

So that a first launch without network has config, embed the latest bundle in your app. Download it unwrapped, as in Config Bundles in CI/CD. The native SDK reads the file:
Add appdna-config.json to your Xcode project as a bundle resource.
The embedded bundle is a fallback only. Cached config takes its place for every section the cache holds, and a remote fetch replaces both. See Offline-First Architecture.

Experiments

AppDNA experiments use deterministic bucketing to assign users to variants. No server call is required — the assignment is computed locally on the device.

How Bucketing Works

The SDK computes a bucket from 0–9999 with MurmurHash3 (32-bit): murmur3_32("<experimentId>.<salt>.<userId>") % 10000. The resulting bucket (0–9999) maps to a variant through the variant weights set in the Console. Before that, the SDK checks that the user is in the experiment: its platforms, its targeting rules and its traffic allocation (a second hash, murmur3_32("<experimentId>.<salt>.<userId>.allocation") % 10000); a user who is not in gets no variant and records no exposure. See Who Is in an Experiment.

Key Properties

  • Deterministic: The same user ID always gets the same variant for the same experiment. No randomness, no server dependency.
  • Cross-platform consistent: A user identified with the same user ID on iOS and Android sees the same variant because the hash function and inputs are identical. Before identify(), each device buckets by its own anonymous ID.
  • Session-stable: The variant does not change between sessions or app restarts while the user ID stays the same.
  • Exposure tracking: The SDK records an exposure event the first time it assigns a variant for an experiment, and again only after the app restarts or reset() is called. Results count each anonymous ID once per experiment.

Servable Surface Experiments

Experiments can also test an entire managed surface — a paywall, onboarding flow, in-app message, or survey — with no app code. The experiment’s control variant references the live surface and the treatment variant carries an alternate configuration. When the app presents that surface, the SDK buckets the user (same deterministic hash) and serves the treatment configuration to the treatment cohort automatically, while everyone else sees the live surface. The treatment configuration is not added to the live surface, so only users bucketed into the treatment see it. Servable-surface experiments render on iOS (1.0.65+) and Android (1.0.37+); Flutter and React Native present these surfaces through the native SDK they wrap. See the Experiments guide. The code-level pattern below remains available for feature flags, custom UI, or logic that isn’t a managed surface:

Delegates and Callbacks

All SDK modules use a delegate/callback pattern to communicate events back to your application. This keeps the SDK non-blocking and lets you respond to events in your own code.
Modules expose protocols with default empty implementations. Conform to only the methods you need:

Environments

AppDNA supports two environments to separate development and production data:
  • Events, experiments, and configuration are completely isolated between environments.
  • Use sandbox during development and testing. Switch to production for App Store / Play Store builds.
  • The SDK determines the environment from the API key prefix — there is no need to set it separately if the key is correct.
Never ship an app to production with a sandbox API key. Sandbox data is periodically purged and is not included in production analytics.

Webhooks

AppDNA can send real-time HTTP callbacks to your server whenever key events occur. Webhooks let you sync data to your backend, trigger workflows, or feed events into third-party tools.

Event Types

AppDNA supports 16 webhook event types spanning the full lifecycle:

Security

Every webhook payload is signed with HMAC-SHA256. Your server should verify the signature before processing:
The signing secret is generated when you create a webhook endpoint in the dashboard and is only displayed once.

Reliability

  • 5 attempts total (1 initial delivery + 4 retries) with backoff at 30s, 2min, 10min, 1hr.
  • Your endpoint must respond with a 2xx status within 30 seconds or the attempt is considered failed.
  • After 50 consecutive failures, the webhook endpoint is automatically disabled (disabled_reason: consecutive_failures). Re-enable it from the dashboard or via the API after fixing the issue.
  • You can view delivery logs and manually retry failed deliveries in the dashboard.
To test webhooks locally during development, use a tunneling tool like ngrok and point your webhook endpoint to the tunnel URL.

Identity

AppDNA manages user identity through a combination of anonymous and authenticated identifiers.

Anonymous ID

On first launch, the SDK generates a random anonymous ID and persists it securely: The anonymous ID survives app restarts and reinstalls (on iOS, as long as the Keychain entry is preserved).

Linking Identity

When you call identify(userId:), the SDK links the anonymous ID to the provided user ID. All past events tracked under the anonymous ID are retroactively associated with the user.

Resetting Identity

Calling reset() clears the user ID but keeps the anonymous ID. This is appropriate for sign-out flows where another user may sign in on the same device.
The anonymous ID is never deleted by reset(). This ensures continuity for device-level analytics even across multiple user sessions.