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.
| Area | SDK provides | Your app provides |
|---|---|---|
| Runtime | OVOK client integration, native polyfills, theme context | Environment values, one client instance, application startup |
| Authentication | Sign-in, registration, recovery, profile and session components | Routes, tenant policy, error messaging, account rules |
| Bluetooth | Device protocols, scan lifecycle, decoded measurements | Device choice, patient association, persistence and upload policy |
| Health import | HealthKit and Health Connect authorization and sync components | Requested data types, code mapping, permission education and retry UX |
| Forms and records | Questionnaire and observation presentation components | Resource selection, app navigation and workflow decisions |
| Background work | Queues and platform scheduling helpers | Durable 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.backgroundSyncand a durable storage adapter. - Android Health Connect scheduled import uses WorkManager and a registered headless task.
- Apple HealthKit observer delivery uses
AppleHealthSync.backgroundDeliveryand 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.