Case study · Mobile · Healthcare
Gloria
A React Native app for the staff, families, and administrators of residential-care communities. One codebase, three roles, and an approval step before anything reaches a family.

The problem
Families with a parent in assisted living or memory care mostly hear nothing between visits. Facilities do have the information: photos, wellness notes, announcements, calendars, supply requests. It lives in shift notes and group texts, and it never reaches the people who want it most. Gloria is the channel. The facility pays per resident; staff post, families follow, and administrators keep an approval gate on anything that goes public.
The hard constraint is that one app has to serve all three roles without feeling like three apps stapled together, on a backend the app team does not own.
My role
I own the mobile app end to end: architecture, every screen, the auth and session layer, push notifications, the release pipeline, and the working relationship with the backend team. Backend changes go to them as written asks with file and line references; I do not touch their repository.
What I built
- Role-based navigation. Staff, family, and admin views share components but have separate route groups and guards, so a family member can never land on a staff screen even by deep link.
- Authentication and session. Auth0 with a native email and password form rather than a web login, a single-flight token refresh so concurrent 403s cannot race token rotation into a forced logout, and biometric unlock behind a feature flag.
- Approval workflow. Staff updates go to a queue; admins approve or deny from a notification that opens the real post in place. A denied update returns to its author as a "revise and resubmit" card rather than a dead end.
- Notifications. Firebase push on both platforms, with every notification type routed to the entity it refers to and opened as a modal on top of the list instead of navigating away.
- Media pipeline. Capture, crop, downscale, and upload through presigned URLs, with a shared photo utility so every screen produces the same output.
- Release process. Weekly over-the-air updates through EAS. Each substantive change runs through an adversarial review pass before it ships; that pass caught an iOS action-sheet race that would have shipped otherwise.
Architecture
Gloria architecture: one Expo app with three role route groups talking to a Spring API through a typed fetch layer, Auth0 for identity and Firebase for push.
Difficult problems
Refresh races. Three screens mounting at once, each receiving a 403 on an expired token, each calling refresh. With rotating refresh tokens the second call invalidates the first and the user is logged out for no reason. The fix is small: one in-flight refresh promise that every caller awaits.
// contexts/AuthContext.tsx (shape, not the full file)
let inflight: Promise<Tokens> | null = null;
async function refreshToken() {
if (inflight) return inflight;
inflight = doRefresh().finally(() => { inflight = null; });
return inflight;
}
Which notification is actionable. The backend emits two approval-shaped events: one tells an admin there is something to review, the other tells the author what happened. They look alike and route to different screens. Getting that wrong meant a denied update opened as if it were a published post. The app now reads the authoritative approval record before deciding what to open.
Reading line counts on iOS. To offer "Read more" on long posts you need to know how many lines the text would take. With a line clamp already applied, iOS reports the clamped count, so overflow can never be detected. The fix measures once without the clamp, then clamps.
Where admin comes from. The API has both a role and a "primary administrator" flag. An audit with citations into the backend showed every admin capability is authorized on the role alone; the flag is read nowhere. Treating the flag as admin in the app would have produced buttons that always fail. The app now derives admin from the role, and the flag waits for the backend to mean something.

Decisions and tradeoffs
- Native login form over Auth0's hosted page. Better feel, one fewer browser hop, and passwords still never touch our servers. Cost: we own the form's edge cases.
- Modals over navigation for notification taps. The user keeps their place in the list. Cost: careful state handling so the same modal can be reused by six entity types.
- Feature flags for messaging, check-in, and security v2. The code ships dark and turns on when the backend is ready, instead of living on a long branch.
- Documenting the app's contracts in the repo. A known-issues tracker and a backend-asks file keep decisions from being re-litigated.
