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
MedicationRequesttest 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
subjectandstatus; 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
profileis missing or not a Patient: wait foruseSessionto 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.