Mobile SDKsConfiguration

Configuration

All Datafly mobile SDKs share the same configuration options.

Configuration options

OptionTypeDefaultDescription
pipelineKeyStringRequiredPipeline key from your source configuration
endpointStringRequiredYour Signal collection endpoint URL (e.g., https://data.example.com)
flushIntervalNumber30Seconds between automatic flushes
flushThresholdNumber20Number of events queued before automatic flush
maxQueueSizeNumber1000Maximum events stored in local SQLite queue
sessionTimeoutNumber1800Seconds of inactivity before starting a new session (30 min)
trackAppLifecycleBooleantrueAuto-track install, update, open, and background events
collectDeviceIdentifiersBooleantrueAttach device identifiers (idfv on iOS, android_id on Android) and the advertising ID
pinnedCertificateHashesString[][]Base64 SHA-256 of pinned SubjectPublicKeyInfo
encryptEventQueueBooleantrueEncrypt queued event payloads at rest
debugBooleanfalseEnable debug logging

Device identifiers

collectDeviceIdentifiers controls whether the SDK attaches hardware-scoped identifiers to events. Set it to false where you do not want a persistent device identifier leaving the app, or where pairing android_id with an advertising ID would complicate your Play Data Safety declaration. When it is off, setAdvertisingId becomes a no-op as well.

iOS

let config = DataflyConfig(
    pipelineKey: "dk_live_abc123",
    endpoint: "https://data.example.com",
    flushInterval: 15,
    flushThreshold: 10,
    maxQueueSize: 5000,
    sessionTimeout: 900,          // 15 minutes
    trackAppLifecycle: true,
    debug: false
)
Datafly.shared.initialize(config: config)

Android

val config = DataflyConfig(
    pipelineKey = "dk_live_abc123",
    endpoint = "https://data.example.com",
    flushIntervalSeconds = 15,
    flushThreshold = 10,
    maxQueueSize = 5000,
    sessionTimeoutSeconds = 900,
    trackAppLifecycle = true,
    debug = false
)
Datafly.initialize(context, config)

React Native

Datafly.initialize({
  pipelineKey: 'dk_live_abc123',
  endpoint: 'https://data.example.com',
  flushInterval: 15,
  flushThreshold: 10,
  maxQueueSize: 5000,
  sessionTimeout: 900,
  trackAppLifecycle: true,
  debug: false,
});

Flutter

await Datafly.initialize(DataflyConfig(
  pipelineKey: 'dk_live_abc123',
  endpoint: 'https://data.example.com',
  flushInterval: 15,
  flushThreshold: 10,
  maxQueueSize: 5000,
  sessionTimeout: 900,
  trackAppLifecycle: true,
  debug: false,
));

.NET MAUI

Datafly.Initialize(new DataflyConfig
{
    PipelineKey = "dk_live_abc123",
    Endpoint = "https://data.example.com",
    FlushInterval = 15,
    FlushThreshold = 10,
    MaxQueueSize = 5000,
    SessionTimeout = 900,
    TrackAppLifecycle = true,
    Debug = false
});

Endpoint URL

The endpoint should be your Signal collection endpoint URL. This is the same endpoint used by the web SDK (Datafly.js):

https://data.example.com

The SDK appends /v1/batch for event delivery. Ensure your Signal collection endpoint is accessible from mobile networks (not just your internal network).

⚠️

Do not include a trailing slash in the endpoint URL. The SDK normalises the endpoint by stripping trailing slashes automatically.

Pipeline key

The pipeline key identifies which source this data belongs to. Create a separate source in the management UI for each mobile app (or use the same source as your web property if you want unified data).

Pipeline keys follow the format: dk_live_ or dk_test_ followed by a random string.

Debug mode

When debug is enabled:

  • iOS: Logs to the Xcode console via os.log (subsystem: com.datafly.signal)
  • Android: Logs to Logcat with tag DataflySignal

You can also attach a callback to inspect events before they are queued:

// iOS
Datafly.shared.onEvent = { event in
    print("Event: \(event)")
}
 
// Android
Datafly.onEvent = { event ->
    Log.d("Datafly", "Event: $event")
}

Disable debug mode in production builds to avoid unnecessary logging overhead.

Challenge tokens

Pipelines with challenge_token_required reject writes that arrive without a token. Supply one with setChallengeToken, and it is sent as X-Datafly-Token on every upload:

Datafly.shared.setChallengeToken(token)
Datafly.setChallengeToken(token)

Pass null to clear it.

⚠️

Enabling challenge_token_required on a mobile pipeline without calling this first will reject every event with a 401.

Mobile pipelines and the domain allowlist

A mobile SDK is not a browser and never sends an Origin header, so the per-pipeline domain allowlist cannot apply to it. Pipelines of type mobile are exempt from the missing-Origin default-deny rule — without that exemption, a mobile pipeline that happened to carry a domain allowlist would reject all of its own traffic with nothing in the response explaining why.

Set the pipeline’s type to Mobile when you create it so this applies.

Transport security

Three controls a security review will ask about.

Egress is enforced, not just intended

The SDK refuses to contact any host other than the endpoint you configure. This is enforced in the transport itself — the request is rejected before it is built, and the TLS handshake is refused for any other host — so “it cannot phone home” is a property that can be demonstrated rather than a claim about the code.

Subdomains are not implicitly allowed, and the check is not prefix-based: collect.example.com.attacker.com is refused.

Certificate pinning

Off by default, and deliberately so: a pin that outlives its certificate takes your app offline, and this SDK cannot manage rotation for your endpoint.

DataflyConfig(
    pipelineKey: "dk_live_...",
    endpoint: "https://collect.example.com",
    pinnedCertificateHashes: [
        "primary-key-hash-base64",
        "backup-key-hash-base64",   // covers your next key
    ]
)

Pins are SHA-256 of the certificate’s SubjectPublicKeyInfo, base64-encoded — the public key, not the whole certificate, so a pin survives renewal with the same key. Any certificate in the chain may match, so pin an intermediate or root rather than a leaf. Extract one with:

openssl s_client -connect collect.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary | base64

Pinning is applied on top of system trust validation, never instead of it: a pinned certificate that has expired or been revoked still fails.

⚠️

Always configure at least one backup pin covering the key you will rotate to. A single pin means your next certificate renewal takes every installed copy of your app offline until users update.

Queue encryption at rest

On by default. The local queue holds whatever properties and traits your app passed, which is frequently PII, and the file is readable on a rooted or jailbroken device and in unencrypted backups.

Payloads are encrypted individually with AES-GCM rather than encrypting the database, which would mean a native dependency the SDK deliberately does not have. Row metadata stays in clear so the queue can still be indexed and trimmed without decrypting.

Keys live in the iOS Keychain and the Android Keystore, so they are not extractable from the app’s data directory. Android needs API 23 for this; below that payloads are stored in clear rather than protected by a key sitting beside the data it protects.

A queue written before encryption was enabled still drains normally.