Most healthcare platforms treat IDD (Intellectual and Developmental Disabilities) service delivery like a generic CRM problem. It isn't. The coordination hierarchy is real: DDS sets policy → Regional Centers execute it → Service Coordinators manage individuals → Providers deliver services. Flatten that model and you get information overload at every level and accountability gaps at every handoff.
LOIS (Lifecycle Operations and Individual Support) is a purpose-built platform that solves this. Here's what the architecture looks like and why each decision was made.
The Five-Role Access Model
LOIS defines five distinct roles, each with its own navigation tree, dashboard, and data scope not just permission toggles on a shared UI:
| Role | Core Responsibility |
|---|---|
| DDS Administrator | System-wide oversight across all Regional Centers |
| Regional Center Admin | Center-level enrollment, coordinator assignment |
| Service Coordinator | Individual caseload, IPP management, alerts |
| Service Provider | Appointments, health monitoring, case notes |
| Individual / Family | Care plan access, self-logged vitals, messaging |
A role-based routing layer maps every authenticated session to a dedicated layout. PostgreSQL row-level security policies enforce the same segmentation at the query layer so role separation is a data guarantee, not just a UI concern.
Stack Overview
Frontend
- Next.js 16 (App Router) + React 19 + TypeScript
- Redux Toolkit for global state, Apollo Client for GraphQL, Axios for REST
- Tailwind CSS + CVA for type-safe component variants
- Recharts for role-specific dashboards
Backend
- NestJS 11 + TypeScript (code-first GraphQL via Apollo Server)
- PostgreSQL + TypeORM with row-level security
- GraphQL Subscriptions over WebSocket for real-time vitals alerts and IPP notifications
Infrastructure
- AWS Cognito for multi-org identity (JWT role claims verified on every resolver)
- AWS S3 with presigned URLs for direct-upload document storage
End-to-end TypeScript means the same type definitions flow from the GraphQL schema → NestJS resolvers → database entities → React component props. In compliance-sensitive domains, this is a correctness requirement, not a preference.
Key Data Modeling Decision: Individual-Centric, Not Center-Centric
This is where most IDD platforms fail. When an individual transfers between Regional Centers (which is routine), their full history should travel with them.
-- The individuals table holds a mutable FK to current center
individuals.current_regional_center_id → regional_centers
-- All history tables reference the individual, not the center
ipp_records.individual_id → individuals
health_vitals.individual_id → individuals
service_authorizations.individual_id → individuals
case_notes.individual_id → individuals
appointment_records.individual_id → individuals
Transferring an individual updates current_regional_center_id. Every historical record stays intact.
IPP Status Is Computed, Not Manually Set
The Individual Program Plan (IPP) is the legal backbone of IDD service delivery. LOIS computes its status dynamically from review_due_date:
function getIPPStatus(reviewDueDate: Date): 'Current' | 'Due Soon' | 'Overdue' {
const daysUntilDue = differenceInDays(reviewDueDate, new Date());
if (daysUntilDue > 30) return 'Current';
if (daysUntilDue >= 0) return 'Due Soon';
return 'Overdue';
}
No coordinator action required. The status surfaces in every dashboard simultaneously the moment a plan goes overdue.
Health Monitoring as a Native Layer
Rather than integrating with an external EHR, LOIS embeds vitals tracking directly. Service Providers see blood pressure, heart rate, temperature, and O₂ saturation for every assigned individual in a single table color-coded alerts at the cell level, elapsed time since last reading, unresolved alert badge counts.
Individuals can log their own readings from their portal. That reading immediately appears in the provider's patient health table and the coordinator's case view. Multiple parties contributing to the same record produces a more complete picture than any single source could.
Individual Portal: Participant, Not Subject
The individual-facing experience uses a horizontal top nav (consumer app pattern) rather than the admin sidebar. Quick action cards are personalized "Message Sarah Johnson" not "Message Your Coordinator." Self-service appointment booking, document uploads, and vitals logging mean the individual contributes to their own record rather than just receiving it.
This is not cosmetic. It reflects a different relationship with the platform. An individual who can see and interact with their own care data shows better care plan adherence.
Real-Time Layer
GraphQL Subscriptions via graphql-ws push three event types without polling:
- Vitals alerts when a reading exceeds range thresholds
- IPP overdue notifications to coordinator dashboards
- Appointment status changes reflected across all relevant roles
RxJS manages the event streams for the notification feed on the client side.
What Gets Built Wrong Most Often
- Flat user models hiding/showing nav items by role still shows users what they can't access. Separate navigation trees per role communicates purpose.
- Center-centric data storage : case history that stays at the origin center when an individual transfers is a care quality failure.
- Manual IPP status : if a coordinator has to update "Overdue" manually, it won't get updated. Compute it.
- Reporting as a separate module : metrics that require navigation + parameter selection + generation won't be used for daily decisions. Surface KPIs in the dashboard directly.
- Individual portal as read-only : a portal with no write capability misses the therapeutic value of active participation.
Full architecture writeup, onboarding flow design, and role-specific feature breakdowns: How to Build an IDD Case Management Platform
Top comments (0)