# Background sync

Canonical page: https://docs.ovok.com/native-sdk/guide/modules/background-sync

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… | Use | Reference |
| --- | --- | --- |
| Deliver Bluetooth readings through a durable queue | `BTProvider.backgroundSync` | [Queue Bluetooth readings](#queue-bluetooth-readings) |
| Keep a Bluetooth scan active on Android | `BTProvider`'s foreground service, or `AndroidBluetoothForegroundService` when you own the scan | [Run an Android BLE foreground service](#run-an-android-ble-foreground-service) |
| Import Health Connect data after Android stops the process | WorkManager plus a registered headless task | [Schedule Health Connect imports](#schedule-health-connect-imports) |
| Import HealthKit updates while the app is backgrounded | `AppleHealthSync.backgroundDelivery` | [Receive HealthKit updates](#receive-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](/native-sdk/guide/how-to/installation)
for the config plugin options and rebuild requirements.

## Queue Bluetooth readings [#queue-bluetooth-readings]

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

~~~tsx
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 [#restore-ios-bluetooth-state]

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

~~~ts
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.

~~~ts
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 [#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:

~~~tsx
<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`](/native-sdk/guide/components/background-sync/) 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](/native-sdk/guide/how-to/installation).
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 [#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:

~~~ts
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:

~~~ts
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](/native-sdk/guide/components/data-sync/android/health-connect-authorization-provider/)
and [Health Connect guide](/native-sdk/guide/modules/data-sync).

`runAndroidHealthConnectSync` performs one import without rendering
[`AndroidHealthSync`](/native-sdk/guide/components/data-sync/android/health-sync/). 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 [#receive-healthkit-updates]

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

~~~tsx
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](/native-sdk/guide/components/data-sync/apple/health-sync/).

## Optional: notify after BLE delivery [#optional-notify-after-ble-delivery]

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

~~~ts
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](/native-sdk/guide/components/background-sync/).

## Before shipping [#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 [#api-reference]

Every export of `@ovok/native/background-sync`, with signatures, props and types, is in the [`background-sync` API reference](/native-sdk/reference/background-sync).
