# Build an app with the SDK

Canonical page: https://docs.ovok.com/native-sdk/guide/how-to/app-building

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](/native-sdk/guide/getting-started) 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](/native-sdk/guide/how-to/installation)
for package peers, Expo configuration, and platform permissions.

## Decide what the SDK owns [#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 [#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](/native-sdk/guide/how-to/app-shell). Initialize the native runtime from the app
entry module before importing this app shell, as shown in
[Polyfills and runtime setup](/native-sdk/guide/components/polyfills/).

```tsx
import { OvokProvider } from "@ovok/core/next/react";
import {
  DEFAULT_COLORS,
  DEFAULT_MULTIPLIERS,
  ThemeProvider as OvokThemeProvider,
} from "@ovok/native";
import { Ovok } from "@ovok/native/next";
import * as SecureStore from "expo-secure-store";
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;
const tenantCode = process.env.EXPO_PUBLIC_TENANT_CODE;
if (!baseUrl || !tenantCode) {
  throw new Error("Set EXPO_PUBLIC_OVOK_BASE_URL and EXPO_PUBLIC_TENANT_CODE");
}

const client = new Ovok({ baseUrl, tenantCode, secureStore: SecureStore });

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/next/react`; 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 [#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.

```tsx
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](/native-sdk/guide/modules/auth).

## 3. Add one Bluetooth measurement flow [#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`:

```tsx
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](/native-sdk/guide/components/bt-management/bluetooth-device-picker/) or an
app-owned chooser before calling its supplied `select` function. Use
[SupportedDevicesCatalogList](/native-sdk/guide/components/ui/supported-devices-catalog-list) to
let people browse catalog metadata, and [BTDeviceList](/native-sdk/guide/components/bt-device/bt-device-list/)
for the SDK's connection-status rows. See [Bluetooth](/native-sdk/guide/modules/bt-management),
[Supported devices and catalog](/native-sdk/guide/modules/bt-device), and
[BTDeviceInstruction](/native-sdk/guide/components/bt-device/bt-device-instruction/) for native
setup, accepted device declarations, and available manuals.

## 4. Decide how measurements become app data [#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](/native-sdk/guide/modules/data-sync) for observation code
mapping and data-sync behavior.

## 5. Add Apple Health or Health Connect import [#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:

```tsx
import { DataSync } from "@ovok/native/data-sync";
import { appleDataToSync } from "./health-sync-config";

export function AppleHealthRoute() {
  return (
    <DataSync.AppleHealthAuthorizationProvider
      readIdentifiers={["HKQuantityTypeIdentifierHeartRate"]}
    >
      <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](/native-sdk/guide/modules/data-sync) for mappings, manual
authorization UI, progress, partial access, and platform setup.

## 6. Build forms and content screens around real app data [#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.

```tsx
import { QuestionnaireForm } from "@ovok/native";
import type { Questionnaire } from "@medplum/fhirtypes";

export function IntakeRoute({ questionnaire }: { questionnaire: 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](/native-sdk/guide/components/ui/) and
[SDK feature map](/native-sdk/guide/sdk-features) for the available surfaces.

## 7. Add background work after foreground flows [#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](/native-sdk/guide/modules/background-sync) before
adding native background services.

## Before shipping [#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](/native-sdk/guide/public-api),
[Bluetooth guide](/native-sdk/guide/modules/bt-management), [health data guide](/native-sdk/guide/modules/data-sync), and
[component reference](/native-sdk/guide/components/ui/).
