Skip to main content

Build an app with the SDK

This guide shows how to grow a working @ovok/native setup into a mobile app. It uses a practical order: initialize the SDK once, add an authenticated route, connect one health workflow, and then expand the app around the data it receives.

Start with the quick start to install the SDK and render sign-in. This page assumes that the app has an OVOK environment URL, a patient tenant code, and a native development build. See installation and native setup for package peers, Expo configuration, and platform permissions.

Decide what the SDK owns​

The SDK provides native integrations, health-specific UI, and typed state. The host app still decides how users move through the app and what happens to the data.

AreaSDK providesYour app provides
RuntimeOVOK client integration, native polyfills, theme contextEnvironment values, one client instance, application startup
AuthenticationSign-in, registration, recovery, profile and session componentsRoutes, tenant policy, error messaging, account rules
BluetoothDevice protocols, scan lifecycle, decoded measurementsDevice choice, patient association, persistence and upload policy
Health importHealthKit and Health Connect authorization and sync componentsRequested data types, code mapping, permission education and retry UX
Forms and recordsQuestionnaire and observation presentation componentsResource selection, app navigation and workflow decisions
Background workQueues and platform scheduling helpersDurable credentials, consent, idempotent upload and user-visible status

The SDK components do not replace your app's navigation, patient selection, data retention, or support policy. Make those decisions at the point where data enters your application.

1. Set up the app shell once​

Keep the client and native managers outside React renders. Mount the providers once above the routes that use the SDK. The full provider order and theme contract are in App shell and providers. Initialize the native runtime from the app entry module before importing this app shell, as shown in Polyfills and runtime setup.

import { OvokClient, OvokProvider } from "@ovok/core";
import {
DEFAULT_COLORS,
DEFAULT_MULTIPLIERS,
ExpoClientStorage,
ThemeProvider as OvokThemeProvider,
} from "@ovok/native";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { KeyboardProvider } from "react-native-keyboard-controller";
import type { PropsWithChildren } from "react";

const baseUrl = process.env.EXPO_PUBLIC_OVOK_BASE_URL;
if (!baseUrl) throw new Error("Set EXPO_PUBLIC_OVOK_BASE_URL");

const client = new OvokClient({
baseUrl,
fhirUrlPath: "/fhir",
storage: new ExpoClientStorage(),
});

export function AppProviders({ children }: PropsWithChildren) {
return (
<KeyboardProvider>
<OvokProvider client={client}>
<OvokThemeProvider
theme={{
colors: DEFAULT_COLORS,
dark: false,
spacingMultiplier: DEFAULT_MULTIPLIERS.spacing,
borderRadiusMultiplier: DEFAULT_MULTIPLIERS.borderRadius,
}}
>
<BottomSheetModalProvider>{children}</BottomSheetModalProvider>
</OvokThemeProvider>
</OvokProvider>
</KeyboardProvider>
);
}

OvokProvider comes from @ovok/core; the UI ThemeProvider comes from @ovok/native. Add your navigation theme and safe-area setup to this shell as your app needs them. BottomSheetModalProvider is a peer provider used by SDK sheets and must be imported from @gorhom/bottom-sheet.

Keep public environment configuration in your app's environment system. Values embedded in the client bundle are visible to users; never put private credentials in EXPO_PUBLIC_ variables.

2. Add sign-in and choose the authenticated destination​

Use the compound sign-in component to keep the SDK's validation and authentication behavior while your app owns navigation and error presentation.

import { SignIn } from "@ovok/native";
import { useRouter } from "expo-router";
import { Alert } from "react-native";

const tenantCode = process.env.EXPO_PUBLIC_TENANT_CODE;

export function SignInScreen() {
const router = useRouter();

if (!tenantCode) throw new Error("Set EXPO_PUBLIC_TENANT_CODE");

return (
<SignIn>
<SignIn.Header>
<SignIn.Header.Title />
<SignIn.Header.Description />
</SignIn.Header>
<SignIn.EmailForm
loginType="Patient"
tenantCode={tenantCode}
onSuccess={() => router.replace("/home")}
onError={(error) => Alert.alert("Sign-in failed", error.message)}
>
<SignIn.EmailForm.Inputs />
<SignIn.EmailForm.ForgotPassword />
<SignIn.EmailForm.SigninButton />
</SignIn.EmailForm>
<SignIn.RegisterLink onPress={() => router.push("/register")} />
</SignIn>
);
}

The destination route and tenant value are app-specific. For registration, MFA, social login, session guards, and account controls, continue to Authentication.

3. Add one Bluetooth measurement flow​

Start with a device the app intends to support. Select it by the catalog's stable ID and pass its acceptedDevices declarations to BTProvider:

import { BleManager } from "react-native-ble-plx";
import { BTProvider } from "@ovok/native/bt-management";
import { SUPPORTED_DEVICES } from "@ovok/native/bt-device";

const bleManager = new BleManager();
const bp2 = SUPPORTED_DEVICES.find(
(device) => device.id === "catalog-viatom-bp2",
);

export function BloodPressureRoute() {
if (!bp2) throw new Error("The selected device is missing from the SDK catalog");

return (
<BTProvider
bleManager={bleManager}
acceptedDevices={bp2.acceptedDevices}
onDeviceFound={(device) => device.connect()}
onResult={(result) => {
const idempotencyKey = result.id;
// Associate with the active patient and persist or upload in app code.
void saveMeasurement({
idempotencyKey,
deviceData: result.deviceData,
measurement: result.data,
});
}}
onError={({ error, deviceData }) => {
reportBluetoothError(error, deviceData);
}}
>
<BloodPressureScreen />
</BTProvider>
);
}

BloodPressureScreen, saveMeasurement, and reportBluetoothError represent the app's screen, persistence, and error policy. The result includes a stable id, deviceData, and decoded data; use the ID as the backend idempotency key if a result can be retried. Keep the BleManager at module scope or in another long-lived owner so route renders do not create competing scans.

For an app that discovers several compatible devices, enable autoConnect, store the onDeviceSelectionRequired callback, and render the SDK's BluetoothDevicePicker or an app-owned chooser before calling its supplied select function. Use SupportedDevicesCatalogList to let people browse catalog metadata, and BTDeviceList for the SDK's connection-status rows. See Bluetooth, Supported devices and catalog, and BTDeviceInstruction for native setup, accepted device declarations, and available manuals.

4. Decide how measurements become app data​

Bluetooth decoding is only one part of a measurement workflow. Before persisting a result, decide which patient it belongs to, whether it needs local retention, and how it is uploaded. Handle duplicate delivery with the stable result ID. If the user can enter a value manually, validate it against the same app-level rules as a device measurement.

For display, MeasurementList accepts data from the host app, and ObservationDetail provides composable detail sections. These components help render records; your app remains responsible for selecting the resources and wiring screen navigation. See Health data and measurements for observation code mapping and data-sync behavior.

5. Add Apple Health or Health Connect import​

Add platform authorization on the route that explains why the app needs the data. Keep each platform's identifiers and mapping in app configuration. For example, an Apple Health screen can use the SDK's automatic authorization prompt:

import { DataSync } from "@ovok/native/data-sync";
import { HKQuantityTypeIdentifier } from "@kingstinct/react-native-healthkit";
import { appleDataToSync } from "./health-sync-config";

export function AppleHealthRoute() {
return (
<DataSync.AppleHealthAuthorizationProvider
readIdentifiers={[HKQuantityTypeIdentifier.heartRate]}
>
<DataSync.AppleHealthSync
dataToSync={appleDataToSync}
/>
</DataSync.AppleHealthAuthorizationProvider>
);
}

health-sync-config contains the app's platform-to-observation mapping. Map each platform data type to the appropriate OVOK observation code; request only the types the feature uses. Use AndroidHealthConnectAuthorizationProvider with AndroidHealthSync on Android. Both providers expose authorization states that your UI should represent, including refusal and partial access where applicable.

If the app needs its own explanation or consent screen before the OS prompt, set skipRequest and renderManualRequestUI together. The render callback receives the request function. Follow the health data guide for mappings, manual authorization UI, progress, partial access, and platform setup.

6. Build forms and content screens around real app data​

Questionnaire forms accept a FHIR Questionnaire; their success callback returns the submitted QuestionnaireResponse for the app to persist or route onward. The example below assumes saveQuestionnaireResponse and reportQuestionnaireError are app handlers.

import { QuestionnaireForm } from "@ovok/native";
import type { QuestionnaireFormProps } from "@ovok/native/questionnaire-response-form";

export function IntakeRoute({ questionnaire }: Pick<QuestionnaireFormProps, "questionnaire">) {
return (
<QuestionnaireForm
questionnaire={questionnaire}
onSuccess={(response) => saveQuestionnaireResponse(response)}
onError={reportQuestionnaireError}
>
<QuestionnaireForm.Header>
<QuestionnaireForm.Header.Title />
<QuestionnaireForm.Header.Subtitle />
</QuestionnaireForm.Header>
<QuestionnaireForm.Content>
<QuestionnaireForm.Content.Item />
<QuestionnaireForm.Content.Error />
</QuestionnaireForm.Content>
<QuestionnaireForm.Navigation>
<QuestionnaireForm.Navigation.PreviousButton />
<QuestionnaireForm.Navigation.NextButton mode="contained" />
<QuestionnaireForm.Navigation.SubmitButton mode="contained" />
</QuestionnaireForm.Navigation>
</QuestionnaireForm>
);
}

The SDK provides other focused presentation components for content, observations, settings, pickers, and guided flows. Compose them where they fit your screen, and keep data fetching, empty states, route transitions, and app-specific completion policy in the host app. Browse the component reference and SDK feature map for the available surfaces.

7. Add background work after foreground flows​

First make the foreground flow understandable and recoverable. Then enable only the background path the feature needs:

  • BLE result delivery uses BTProvider.backgroundSync and a durable storage adapter.
  • Android Health Connect scheduled import uses WorkManager and a registered headless task.
  • Apple HealthKit observer delivery uses AppleHealthSync.backgroundDelivery and native HealthKit configuration.

Background callbacks may run without the React tree or in-memory session. The app must restore the minimum client and patient context needed to process work, persist queued results, and make uploads idempotent. Keep a foreground status and retry path for work that cannot complete. Read the background sync guide before adding native background services.

Before shipping​

Exercise the complete workflow on every native platform you support:

  • fresh install, sign-in, sign-out, and expired-session recovery;
  • permission allowed, denied, and partially granted;
  • one live device measurement and, where supported, one history measurement;
  • duplicate delivery and app restart during queued work;
  • offline pause, recovery, and retry;
  • small screens, keyboard behavior, safe areas, dark theme, and accessibility;
  • translated labels and useful empty, loading, and error states.

For implementation details, continue to the public API map, Bluetooth guide, health data guide, and component reference.