Architecture
This page explains how the Member Portal is structured at a high level — how the layout is assembled, how authentication is enforced, and how pages communicate with each other.
Overall layout
The app uses Mantine's AppShell to create the standard three-part layout:
┌─────────────────────────────────────────────┐
│ Header (52px) │
├──────────────┬──────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ (260px or │ (Router renders │
│ 60px mini) │ the active page) │
│ │ │
└──────────────┴──────────────────────────────┘
- Header — shows the ChartChat logo, the current page title, and the profile dropdown. On mobile, also shows a hamburger menu.
- Sidebar — shows navigation links grouped into Overview, My Health, and Account. Can be collapsed to icon-only mode on desktop.
- Main — where each page renders. The
<Router />component decides which page to show based on the URL.
How authentication works
App.tsx is the root component. It checks whether the user is logged in using the Medplum client:
medplum.isLoading() → show nothing (waiting for session check)
medplum.getProfile() → null means not logged in → show public routes
medplum.getProfile() → returns Patient → show the full app
Public routes (visible without being logged in):
/→ Sign In page/register→ Sign Up page/setpassword/:id/:secret→ Set Password page/verifyemail/:id/:secret→ Verify Email page- Any other URL redirects to
/(sign in)
Protected routes (require login): Everything else — Dashboard, Lab Results, Medications, etc.
Provider hierarchy
The app wraps everything in React context providers. Here is the order from outermost to innermost:
MedplumProvider ← Medplum SDK context (FHIR client, auth session)
NotificationProvider ← persistent bell notifications (FHIR-backed)
HospitalContextProvider ← tracks hospital-level EHR connections
EhrContextProvider ← tracks vendor-level EHR connections (Epic, Healow, MEDITECH)
PatientViewProvider ← patient/expert view mode toggle
HeaderProvider ← lets any page set the header title and action button
IdleTimeoutGuard ← monitors inactivity and auto-logs out after timeout
AppShell ← the visual layout (header + sidebar + main)
Router ← renders the active page
Each layer is independent — HeaderProvider only manages the title text, EhrContextProvider only manages vendor connections, HospitalContextProvider only manages hospital connections, etc.
Idle timeout
Every authenticated session is protected by an idle timeout. If the patient has no mouse, keyboard, touch, or scroll activity for the configured duration (default: 15 minutes), a warning modal appears with a countdown. If they don't respond, they are automatically signed out.
The timeout duration can be changed in Settings. Available options: 15 min, 30 min, 60 min, 4 hours.
See useIdleTimeout for implementation details.
How pages set the header title
Each page calls setPageTitle(...) from useHeader() when it mounts, and clears it when it unmounts:
const { setPageTitle, setHeaderAction } = useHeader();
useEffect(() => {
setPageTitle('Lab Results');
setHeaderAction(<SomeButton />);
return () => {
setPageTitle('');
setHeaderAction(null);
};
}, []);
This lets each page control what appears in the top header without the layout needing to know anything about the page.