Skip to main content

Step 5: Initialise Ovok and authentication

What we are building​

One persisted Ovok client, patient sign-in and registration screens, and a protected patient area that reads the authenticated Patient's plan.

What you should already have​

  • The tenant code and sandbox API URL from step 2.
  • The CMS loader from step 4.
  • Published sandbox MedicationRequest test records created by an authorized operator.

The implementation​

Configure one client​

Create src/config/ovok.ts:

export const ovokConfig = {
baseUrl: process.env.EXPO_PUBLIC_OVOK_BASE_URL!,
tenantCode: process.env.EXPO_PUBLIC_TENANT_CODE!,
};

Create src/client/ovok-client.ts:

import { OvokClient } from "@ovok/core";
import { ExpoClientStorage } from "@ovok/native/polyfills";

import { ovokConfig } from "../config/ovok";

export const ovokClient = new OvokClient({
baseUrl: ovokConfig.baseUrl,
fhirUrlPath: "/fhir/R4/",
storage: new ExpoClientStorage(),
});

The client owns the active login and persisted SDK session. Keep one instance at module scope. See Core client configuration.

The current Expo template keeps routes in src/app; mount the providers in src/app/_layout.tsx:

import { OvokProvider } from "@ovok/core";
import { DEFAULT_COLORS, DEFAULT_MULTIPLIERS, ThemeProvider as OvokThemeProvider } from "@ovok/native";
import { I18nextProvider } from "react-i18next";
import { Slot } from "expo-router";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";

import { LocalizationGate } from "../localization/localization-gate";
import i18n from "../localization/i18n";
import { ovokClient } from "../client/ovok-client";

export default function RootLayout() {
return (
<KeyboardProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<OvokProvider client={ovokClient}>
<OvokThemeProvider
theme={{
colors: DEFAULT_COLORS,
dark: false,
spacingMultiplier: DEFAULT_MULTIPLIERS.spacing,
borderRadiusMultiplier: DEFAULT_MULTIPLIERS.borderRadius,
}}
>
<I18nextProvider i18n={i18n}>
<LocalizationGate>
<Slot />
</LocalizationGate>
</I18nextProvider>
</OvokThemeProvider>
</OvokProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
</KeyboardProvider>
);
}

The Native SDK auth forms need the SDK theme provider and the native keyboard/gesture setup. The provider values above match the current Native SDK app shell; ThemeProvider also requires @expo-google-fonts/dm-sans, installed in step 1.

Protect the patient route group in src/app/(patient)/_layout.tsx. Keep an offline session available—the Native SDK reports isAuthenticated for a persisted offline session—while still requiring a Patient profile:

import { useSession } from "@ovok/native";
import { Redirect, Slot } from "expo-router";
import { ActivityIndicator } from "react-native";

export default function PatientLayout() {
const { status, isAuthenticated, profile } = useSession({ sessions: false });
if (status === "loading") return <ActivityIndicator />;
if (!isAuthenticated || profile?.resourceType !== "Patient") return <Redirect href="/sign-in" />;
return <Slot />;
}

The registration form is the account and Patient initialization path for this tutorial. Do not follow a successful registration by creating a second Patient resource in the app.

Use the Native SDK authentication forms​

Create src/app/sign-in.tsx with the supported patient form and src/app/register.tsx with the registration form below. Replace the Expo starter routes with these screens and the authenticated src/app/(patient)/ route group so each URL resolves to one flow:

import { SignIn, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { ActivityIndicator, View } from "react-native";

import { ovokConfig } from "../config/ovok";

export default function SignInScreen() {
const router = useRouter();
const { status, isAuthenticated } = useSession({ sessions: false });

if (status === "loading") return <ActivityIndicator />;
if (isAuthenticated) return <Redirect href="/(patient)" />;

return (
<View>
<SignIn>
<SignIn.Header>
<SignIn.Header.Title />
<SignIn.Header.Description />
</SignIn.Header>
<SignIn.EmailForm
loginType="Patient"
tenantCode={ovokConfig.tenantCode}
onSuccess={() => router.replace("/(patient)")}
>
<SignIn.EmailForm.Inputs />
<SignIn.EmailForm.SigninButton />
</SignIn.EmailForm>
</SignIn>
</View>
);
}

For the sandbox's patient registration screen, use Register.EmailForm with the same tenantCode and route to the patient area from onSuccess. Use the SDK's form rather than sending passwords through a custom FHIR request. See Native authentication, patient registration, and patient login.

Load the signed-in Patient's plan​

Put the loader in src/features/medication/medication-plan.ts so Today, Details, and History screens share one query and step 6 can infer its resource type:

import type { OvokClient } from "@ovok/core";

export async function loadMedicationPlan(client: OvokClient, patientId: string) {
const requests: Awaited<ReturnType<typeof client.searchResources>> = [];
for await (const page of client.searchResourcePages("MedicationRequest", {
subject: `Patient/${patientId}`,
status: "active",
_count: "100",
})) {
requests.push(...page);
}
return requests;
}

Create src/features/medication/use-medication-plan.ts. Derive the Patient from the active Native SDK session and load the plan with that id:

import { useClient } from "@ovok/core";
import { useSession } from "@ovok/native";
import * as React from "react";

import { loadMedicationPlan } from "./medication-plan";

export function useMedicationPlan() {
const client = useClient();
const { status, isAuthenticated, profile } = useSession({ sessions: false });
const patientId = profile?.resourceType === "Patient" ? profile.id : undefined;
const [plan, setPlan] = React.useState<Awaited<ReturnType<typeof loadMedicationPlan>>>([]);
const [error, setError] = React.useState<Error>();
const [loading, setLoading] = React.useState(true);

React.useEffect(() => {
if (status === "loading") return;
if (!isAuthenticated || !patientId) {
setPlan([]);
setError(undefined);
setLoading(false);
return;
}
let current = true;
setLoading(true);
setError(undefined);
setPlan([]);
void loadMedicationPlan(client, patientId)
.then((resources) => { if (current) setPlan(resources); })
.catch((reason: unknown) => { if (current) setError(reason instanceof Error ? reason : new Error(String(reason))); })
.finally(() => { if (current) setLoading(false); });
return () => { current = false; };
}, [client, isAuthenticated, status, patientId]);

return { plan, patient: patientId ? profile : undefined, loading, error };
}

Keep the Patient type check; do not assume every authenticated account is a Patient. See Native authentication for the current session and form APIs.

Add patient registration​

When self-registration is enabled for the sandbox, the Native SDK form creates and activates the Patient account. It does not require a second client-side Patient create:

import { Register, useSession } from "@ovok/native";
import { Redirect, useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Alert, View } from "react-native";

import { ovokConfig } from "../config/ovok";

export default function RegisterScreen() {
const router = useRouter();
const { t } = useTranslation();
const { status, isAuthenticated } = useSession({ sessions: false });

if (status === "loading") return <ActivityIndicator />;
if (isAuthenticated) return <Redirect href="/(patient)" />;

return (
<View>
<Register>
<Register.Header>
<Register.Header.Title />
<Register.Header.Description />
</Register.Header>
<Register.EmailForm
tenantCode={ovokConfig.tenantCode}
onSuccess={() => router.replace("/(patient)")}
onError={() => Alert.alert(t("register.registrationFailed"), t("register.tryAgain"))}
>
<Register.EmailForm.Inputs />
<Register.EmailForm.RegisterButton />
</Register.EmailForm>
<Register.LoginLink onPress={() => router.push("/sign-in")} />
</Register>
</View>
);
}

If your sandbox uses pre-provisioned Patients instead, omit self-registration and use the test account. For production, protect the patient route group with the session state and send signed-out users to the sign-in route.

The protected route group created earlier is src/app/(patient)/_layout.tsx:

import { useSession } from "@ovok/native";
import { Redirect, Slot } from "expo-router";
import { ActivityIndicator } from "react-native";

export default function PatientLayout() {
const { status, isAuthenticated, profile } = useSession({ sessions: false });
if (status === "loading") return <ActivityIndicator />;
if (!isAuthenticated || profile?.resourceType !== "Patient") {
return <Redirect href="/sign-in" />;
}
return <Slot />;
}

Important Ovok decisions​

  • The authenticated profile supplies the Patient id. Do not accept a patient id from a route parameter as the authorization context.
  • The search includes subject and status; the AccessPolicy from step 2 must independently enforce the patient scope.
  • Native SDK forms own the authentication exchange. The app only chooses navigation and displays loading/error states.

Expected result​

The sandbox patient can sign in, enter a protected patient area, and read only their active MedicationRequest resources.

Common errors and troubleshooting​

  • profile is missing or not a Patient: wait for useSession to finish, then route non-patient account types away from patient screens.
  • 401 or 403 on search: check base URL/FHIR path, login state, and the patient AccessPolicy. Do not test with an administrator token and assume the policy works.
  • Authentication works but plan is empty: verify the synthetic request's subject reference and active status.

Previous / Next​

Previous: step 4: set up localisation and time handling · Continue to step 6: build the Today screen.