GuidesModules

Background sync

Choose and configure background delivery for Bluetooth and health data

  • Stable unknown
  • Release candidate since 1.16.1-rc.3
  • Development since 1.16.1-dev.2111+9796905

Background work is a set of platform-specific delivery paths, not a single SDK scheduler. Pick the path that matches the data source, configure its native capabilities, and restore the application state it needs when the operating system starts work outside the foreground app.

Your app needs to…UseReference
Deliver Bluetooth readings through a durable queueBTProvider.backgroundSyncQueue Bluetooth readings
Keep a Bluetooth scan active on AndroidBTProvider's foreground service, or AndroidBluetoothForegroundService when you own the scanRun an Android BLE foreground service
Import Health Connect data after Android stops the processWorkManager plus a registered headless taskSchedule Health Connect imports
Import HealthKit updates while the app is backgroundedAppleHealthSync.backgroundDeliveryReceive HealthKit updates

The operating system controls when background work runs. These APIs do not promise continuous execution or an exact schedule. See installation and native setup for the config plugin options and rebuild requirements.

Queue Bluetooth readings

BTProvider.backgroundSync persists each result before calling its asynchronous onResult handler. Supply durable storage so the queue survives process death:

import { BTProvider, IntegratedDevices } from "@ovok/native/bt-management";

const acceptedDevices = [IntegratedDevices.BP2] as const;

<BTProvider
  bleManager={bleManager}
  acceptedDevices={acceptedDevices}
  backgroundSync={{
    enabled: true,
    storage: {
      getItem: (key) => measurementStore.getItem(key),
      setItem: (key, value) => measurementStore.setItem(key, value),
    },
    getOwnerKey: () => authStore.currentAccountKey,
    onResult: async (result) => {
      await uploadMeasurement(result, {
        accountKey: result.ownerKey,
        idempotencyKey: result.id,
      });
    },
    maxRetries: 3,
    retryBackoffMs: 30_000,
  }}
  onError={({ error }) => reportBluetoothError(error)}
>
  <MeasurementScreen />
</BTProvider>;

The store must implement asynchronous getItem and setItem and persist data across app restarts. Choose storage and retention rules that fit the sensitivity of your measurement data. The onResult promise is the queue's acknowledgement: resolve it only when your server has accepted the result. Reject it to leave the item queued for retry.

Delivery is at least once. The queue removes an item after onResult resolves, so a process exit between server acceptance and local removal can deliver it again. Use result.id as a server-side idempotency key. queueId identifies one enqueue operation; attempt is the current delivery attempt; queuedAt is the enqueue time. The default is three attempts with a 30-second exponential backoff base. Exhausted items remain persisted and are retried when the provider flushes the queue, such as when it mounts, the app returns to the foreground, or network connectivity returns. If the signed-in account changes while the app remains online, call useBackgroundSync().flush() from a component inside BTProvider to retry entries owned by that account. It retries pending and exhausted entries immediately, resetting their retry delay, and resolves when its current delivery pass finishes. The SDK also starts a flush when network connectivity returns.

The coordinator checks getOwnerKey() before every queued delivery, so a mid-flush account switch stops later readings from being handed to the new account. Each backgroundSync.onResult value also includes the original ownerKey; route or save the reading with that key rather than looking up whichever account is active when your callback runs. A callback already in progress cannot be canceled by an account switch, so it must use the supplied owner key for the entire request. Mismatched readings remain queued until their owner is active again.

A coordinator also tells what it still holds: getQueued() lists the current account's results that are waiting to be accepted, and subscribe(listener) reports each change (the first subscriber reads the stored queue in). useQueuedResults(coordinator) wraps both for a screen that shows readings not saved yet.

getOwnerKey is required: a BackgroundSyncCoordinator created without it throws, rather than refusing every result as it arrives. The account it names has to outlast the moments background work runs in. The native next client retains its saved session during a temporary refresh outage; use its tenant, project, and profile references when deriving a stable owner key for your queue.

New results capture getOwnerKey() when they enter the queue, before waiting for storage or another delivery. Results left by older SDK versions without an owner cannot be safely assigned to an account. On the first flush while an account is available, the SDK drops those legacy results and reports their count through backgroundSync.onError with code BluetoothErrorCode.BACKGROUND_SYNC; it never guesses which account owns them. Delivery errors also report that stable code and are retried according to the configured limit. Retry metrics include the error code, whether another attempt will run, and whether the item is exhausted. backgroundSync.onError receives a BluetoothErrorCode.BACKGROUND_SYNC error for each delivery failure; inspect its originalError for the underlying transport or native details.

BTProvider.onResult is a separate immediate measurement callback. With background sync enabled it still fires when a measurement arrives, while backgroundSync.onResult handles acknowledged queue delivery. Keep these roles separate if you need server delivery to be durable.

Restore iOS Bluetooth state

Create the BLE manager once, outside React renders, with a stable restoration identifier:

import { createBackgroundBleManager } from "@ovok/native/bt-management";

export const bleManager = createBackgroundBleManager({
  restoreStateIdentifier: "com.example.app.bluetooth",
});

Use the same identifier in backgroundSync.restoreStateIdentifier. Enable bluetooth.background in the Expo config plugin and rebuild the native app. The SDK restores known peripherals and flushes queued results when iOS restores the BLE manager. Keep BTProvider mounted for the monitoring session and make its callback and storage dependencies stable.

iOS background scanning needs service UUID filters, and Android needs them while the screen is off. In the background the SDK scans for the services of every accepted device it knows them for: built-in devices from its own table, custom device definitions from mainServices (getBackgroundServiceUUIDs returns the list). A device whose services aren't known is found by the foreground scan only; add the service it advertises to backgroundSync.serviceUUIDs to find it in the background too. The foreground scan is never filtered.

For devices that do not advertise a service UUID, or advertise it only briefly after a measurement, keep a connection request pending instead of relying on the filtered scan. On iOS, keepConnecting asks CoreBluetooth to connect to each device without a timeout, so the system reconnects it whenever it is in range. A device that connects is handled like one the scan found, and the request is made again if it drops the link. It does nothing on Android.

import { useBluetoothRuntime } from "@ovok/native/bt-management";

const runtime = useBluetoothRuntime();

runtime.manager?.keepConnecting(pairedDeviceIds);

Disconnecting a device with disconnectDevice, forget, or stopScan ends its request; call keepConnecting again to resume it.

CoreBluetooth decides when it restores the app and delivers events. A restoration identifier and background mode do not guarantee continuous scanning.

Run an Android BLE foreground service

When backgroundSync.enabled is true, BTProvider manages the Android connected-device foreground service with the scan. Configure its notification and optional battery optimization request in the provider options:

<BTProvider
  bleManager={bleManager}
  acceptedDevices={acceptedDevices}
  backgroundSync={{
    enabled: monitoring,
    storage: measurementStorage,
    onResult: uploadMeasurement,
    android: {
      foregroundService: {
        notificationTitle: "Bluetooth monitoring",
        notificationBody: "Listening for health measurements",
        requestBatteryOptimizationExemption: true,
      },
    },
  }}
>
  <MeasurementScreen />
</BTProvider>

The requestBatteryOptimizationExemption option asks Android to show its system consent UI while the app is active; it does not grant an exemption. Explain the request in your product UI and check the current state with isAndroidBatteryOptimizationIgnored after the app resumes. The foreground service can run without the exemption, subject to Android's power policies.

If your app owns the BLE manager and scan lifecycle outside BTProvider, use the standalone AndroidBluetoothForegroundService or the start/stop functions from @ovok/native/background-sync. Use one service owner per scan; BTProvider already starts its own service. Android service permissions must be present in the native build. See installation and native setup. Changing an inline onError callback does not restart the service. If the component unmounts while the battery-exemption prompt is pending, the service is not started.

Schedule Health Connect imports

Android Health Connect background work is separate from Bluetooth. WorkManager wakes a headless JavaScript task; that task must restore the authenticated client and patient identity because there is no React tree and the SDK cannot serialize a client or credentials.

Register the task once from the application entry point, before Android needs to start the headless runtime:

import {
  registerAndroidHealthConnectBackgroundTask,
  runAndroidHealthConnectSync,
} from "@ovok/native/background-sync";

registerAndroidHealthConnectBackgroundTask(async ({ reason }) => {
  const { client, patientId } = await restoreHealthSyncSession();
  reportBackgroundRun(reason);

  await runAndroidHealthConnectSync({
    client,
    patientId,
    dataToSync: androidDataToSync,
    onError: reportHealthSyncError,
  });
});

Schedule periodic work from a foreground flow after the user enables it:

import {
  cancelAndroidHealthConnectSync,
  scheduleAndroidHealthConnectSync,
} from "@ovok/native/background-sync";

await scheduleAndroidHealthConnectSync({
  intervalMinutes: 15,
  notificationTitle: "Health data sync",
  notificationBody: "Syncing recent Health Connect records",
});

// When the user turns scheduled import off:
await cancelAndroidHealthConnectSync();

WorkManager's minimum interval is 15 minutes; it may run later because Android applies its own scheduling and power policies. The worker also requires network connectivity. If you pass a custom taskName to scheduleAndroidHealthConnectSync, register the headless task with that same name. The default is OvokHealthConnectBackgroundSync.

Android runs the headless task through a foreground service and shows a notification while the work runs. Set notificationTitle and notificationBody when scheduling to provide app-appropriate copy.

Request Health Connect's separate background-read permission with backgroundAccess on AndroidHealthConnectAuthorizationProvider, in addition to the foreground record permissions. Configure the Health Connect background service in the Expo plugin and rebuild. See the authorization provider and Health Connect guide.

runAndroidHealthConnectSync performs one import without rendering AndroidHealthSync. Restore the native Ovok session and pass its matching Patient ID; the import rejects a different patient. Pass the same dataToSync mapping used by the foreground component. onProgress and onError can report headless work to app-owned logging or persistence; do not depend on screen state to observe the result.

Receive HealthKit updates

AppleHealthSync can enable HealthKit observer delivery for each mapped sample type:

import { UpdateFrequency } from "@kingstinct/react-native-healthkit";
import { DataSync } from "@ovok/native/data-sync";

<DataSync.AppleHealthAuthorizationProvider readIdentifiers={healthKitTypes}>
  <DataSync.AppleHealthSync
    dataToSync={appleDataToSync}
    backgroundDelivery={{ frequency: UpdateFrequency.hourly }}
    onProgress={setProgress}
    onError={reportHealthSyncError}
  />
</DataSync.AppleHealthAuthorizationProvider>;

Enable healthKit: { background: true } in the Expo config plugin, provide the HealthKit usage descriptions, and rebuild. Keep AppleHealthSync mounted for as long as the app should receive observer updates: unmounting it removes its subscriptions and disables background delivery for the mapped identifiers. HealthKit and iOS choose when to deliver an observer update; the frequency is a request, not a timer. See the AppleHealthSync reference.

Optional: notify after BLE delivery

The BLE queue can schedule a local notification after backgroundSync.onResult accepts a measurement:

backgroundSync: {
  enabled: true,
  storage: measurementStorage,
  onResult: uploadMeasurement,
  notifications: {
    enabled: true,
    title: "New measurement",
    body: (result) => `${deviceTitle(result.deviceData)} reported a result`,
    data: (result) => ({ resultId: result.id }),
  },
}

Without a body, the SDK uses the same wording with deviceTitle, so the notification names the product (for example "American Health Gate AHG-427B reported a measurement") rather than the decoder key.

Install and configure expo-notifications, request notification permission from a foreground screen, and enable the notifications config plugin option. These are local notifications; they do not start background work or provide remote push tokens. The standalone permission and notification functions are listed in the background-sync API reference.

Before shipping

  • Use durable storage for queued BLE results and deduplicate uploads by result.id.
  • Resolve the queue callback only after the server accepts the result.
  • Build the native app with only the Bluetooth, HealthKit, Health Connect, service, and notification capabilities the app uses.
  • Register the Health Connect headless task from the application entry point and restore its client, patient identity, and data mapping there.
  • Keep HealthKit observer components mounted while observer delivery is needed.
  • Explain foreground service notifications and any battery optimization request to users.

API reference

Every export of @ovok/native/background-sync, with signatures, props and types, is in the background-sync API reference.

Last updated on

On this page