A full-stack healthcare application for managing the lifecycle of patient referrals — from the moment a specialist raises a referral request, through coordinator routing to an in-network facility, to appointment scheduling and completion.
The core problem it addresses is referral leakage: patients who get referred out of a hospital network never complete their appointment, or end up seen at an out-of-network facility. Leakage means lost continuity of care for the patient and lost revenue for the network. This system keeps the referral inside a tracked, auditable workflow and gives administrators the analytics to see where patients are dropping off.
Built as a capstone project during an internship training program.
- The Problem It Solves
- Roles & Workflow
- Referral Lifecycle
- Architecture
- Tech Stack
- Data Model
- API Surface
- Security
- Project Structure
- Getting Started
- Configuration
In a hospital network, a specialist often needs to send a patient to another specialty (e.g. a cardiologist refers a patient to endocrinology). In many systems this handoff happens over phone, fax, or a loose EMR note, and nobody owns the follow-through. The referral "leaks":
- The patient is never scheduled.
- The patient is scheduled at an out-of-network facility, so revenue leaves the system.
- The referral sits in an ambiguous state with no aging or accountability.
This platform makes the handoff a first-class, tracked entity:
- Every referral has an explicit status and an owning coordinator.
- Coordinators route referrals to the nearest in-network facility that actually staffs the requested specialty.
- Specialist matching is specialty-aware, so a referral only goes to a provider who can treat it.
- Every state transition is written to an audit log.
- An admin analytics layer surfaces leakage rate, referral aging, specialty load, and appointment throughput so the network can act on the drop-off points.
The system is built around four roles, each with its own dashboard and a strictly scoped set of permissions:
| Role | What they do |
|---|---|
| Specialist | Looks up patients by MRN, raises referral requests to another specialty, manages their assigned referrals, and runs their daily appointment schedule (viewing slots, completing appointments). |
| Referral Coordinator | The routing engine of the system. Reviews incoming referral requests, picks a destination facility from a distance-ranked, specialty-filtered list, assigns a matching specialist, rejects invalid requests, and can fan a single request out to multiple facilities at once. |
| Patient | Tracks their own referrals and appointments, sees referral status history, and maintains their profile. |
| Admin | Manages users and consumes the analytics layer — leakage, aging, specialty load, appointment analytics, top specialists, daily/monthly referral trends. |
A referral moves through an explicit state machine (backed by the ReferralStatus table):
Requested → Submitted → Accepted → Scheduled → Completed → Closed
└→ Rejected
- Requested — a specialist raises the referral (patient + target specialty + urgency + reason).
- Submitted — the coordinator has routed it to a destination facility.
- Accepted / Rejected — the destination side takes ownership, or the coordinator rejects it.
- Scheduled — an appointment is booked into a specialist's shift block.
- Completed / Closed — the visit happened and the referral is closed out.
Urgency is a first-class dimension (Routine, Urgent, Emergency) and drives prioritisation and aging analytics.
A subtle but important business rule lives in the referral detail logic: which facility is shown as the "primary" facility depends on status. Before a referral is Accepted it reflects the origin facility; once accepted it flips to the destination facility — so the UI always shows the party currently responsible for the patient.
When a coordinator isn't sure which facility will accept, they can route one referral request to several facilities in a single action. The backend stamps all the resulting referrals with a shared ReferralGroupId (a Guid), so the sibling referrals can be tracked and reconciled as one logical request.
┌────────────────────────┐ HTTPS / JWT ┌────────────────────────────┐
│ Vue 3 SPA (Vite) │ ───────────────────────► │ ASP.NET Core Web API │
│ │ │ │
│ • Role-based router │ │ Controllers │
│ • Axios API layer │ ◄─────────────────────── │ → Services (business) │
│ • Tailwind UI │ JSON DTOs │ → EF Core → SQL Server │
└────────────────────────┘ └────────────────────────────┘
The backend follows a conventional layered design:
- Controllers are thin — they authorize, bind DTOs, and delegate.
- Services hold all business logic (routing, specialist matching, referral state transitions, analytics aggregation) and are wired through constructor DI, registered centrally in
Extensions/ServiceRegistration.cs. - EF Core (
AppDbContext) handles persistence against SQL Server, with a set of database views (vw_Coordinator_RequestedReferrals,vw_Specialist_AssignedPatients,vw_AppointmentDetails) mapped as keyless entities to push heavy read-side joins into the database. - A global exception middleware translates typed exceptions (
NotFoundException,BadRequestException,ForbiddenException,UnauthorizedException) into consistent HTTP error responses, so services canthrowdomain errors instead of hand-rolling status codes. - Serilog writes rolling daily log files (7-day retention).
The frontend mirrors the backend's role split: pages under pages/{admin,specialist,referral-coordinator,patient}, a typed API client per domain under src/api, and a route guard that enforces role access before a page ever loads.
Backend
- ASP.NET Core 8 Web API (C#, nullable + implicit usings enabled)
- Entity Framework Core 8 (SQL Server provider)
- JWT bearer authentication (
Microsoft.AspNetCore.Authentication.JwtBearer) - BCrypt.Net for password hashing
- Serilog (file sink) for structured logging
- Swashbuckle / Swagger for API documentation
Frontend
- Vue 3 (Composition API,
<script setup>) + TypeScript - Vite build tooling
- Vue Router 5 with role-based navigation guards
- Tailwind CSS 4
- Axios with a shared interceptor layer;
jwt-decodefor client-side claims
Database
- SQL Server — schema, seed data, views, and indexes managed as raw SQL scripts under
Database/Scripts
Core tables (see Database/Scripts/CreateTable.sql):
- Identity & roles —
User,Role, and role-specific profile tables (Admin,Specialist,ReferralCoordinator,Patient). - Network topology —
GlobalNetwork→Hospital→Facility, withLatitude/Longitudeon facilities to power distance-based routing. - Clinical taxonomy —
Specialty(Cardiology, Dermatology, Oncology, …) and theSpecialistSpecialitieslink table so one specialist can hold multiple specialties. - Referral flow —
Referral,ReferralAssignment,ReferralStatus,UrgencyLevel. - Scheduling —
Appointment,AppointmentStatus, andShiftBlock(three 8-hour blocks covering a 24-hour day). - Compliance —
AuditLogfor a trail of referral state changes.
Read-heavy screens are served through SQL views and supported by 15 indexes (indexes.sql) on the hot query paths (referral lookups, coordinator queues, appointment detail joins).
All endpoints are under /api and are role-gated with [Authorize(Roles = ...)]. Highlights:
| Area | Endpoints (examples) |
|---|---|
| Auth | POST /api/auth/register, POST /api/auth/login, GET /api/auth/me |
| Specialist | GET /api/patient/lookup/{mrn}, POST /api/specialist/referral-intake, GET /api/specialist/specialities, GET /api/referral/my-referrals |
| Coordinator | GET /api/referral/requested, GET /api/referral/specialists/{referralId}, GET /api/referral/{referralId}/facilities-dropdown, POST /api/referral/route, POST /api/referral/{referralId}/reject |
| Appointments | GET /api/appointments/available-slots/{specialistId}/{date}, POST /api/appointments, GET /api/appointments/schedule/{date}, PUT /api/appointments/{id}/complete |
| Patient | GET /api/patient/dashboard, GET /api/patient/referrals, GET /api/patient/appointments/upcoming |
| Admin analytics | GET /api/admin/dashboard, GET /api/admin/analytics/referral-leakage, /facility-leakage, /specialty-load, /referral-aging, /scheduled-delays, /top-specialists, /daily-referrals |
Interactive docs (with a Bearer auth flow) are available at /swagger when running in Development.
- Authentication — stateless JWT bearer tokens. On login the API issues a signed token carrying the user's id and role claims; tokens expire after the configured
Jwt:ExpiryMinutes. - Authorization — every controller action is decorated with role requirements, so the API is the source of truth for access control; the Vue router guard is a UX convenience layered on top, not the security boundary.
- Password storage — passwords are hashed with BCrypt; plaintext is never persisted.
- Registration integrity — user + role-profile creation runs inside a database transaction, so a half-created account can never leak through.
- CORS — locked to the Vite dev origin via the named
VuePolicy. - Transport — HTTPS redirection is enforced in the pipeline.
- Auditability — referral state changes are recorded in
AuditLog, which matters in a healthcare context where who-changed-what-and-when is a compliance concern.
Referral-Management/
├── Backend/
│ └── Referral-Management.Api/
│ └── Referral-Management.Api/
│ ├── Controllers/ # thin HTTP endpoints, role-gated
│ ├── Services/ # business logic (routing, matching, analytics)
│ ├── Models/ # EF Core entities + AppDbContext + views
│ ├── DTOs/ # request/response contracts
│ ├── Migrations/ # EF Core migrations
│ ├── Middleware/ # global exception handling
│ ├── Exceptions/ # typed domain exceptions
│ ├── Extensions/ # DI / service registration
│ └── Program.cs
├── Frontend/
│ ├── src/
│ │ ├── pages/ # per-role screens
│ │ ├── components/ # per-role UI components
│ │ ├── api/ # typed Axios clients
│ │ ├── router/ # role-based route guards
│ │ ├── types/ # shared TypeScript contracts
│ │ └── utils/ # auth, dates, error handling
│ └── package.json
└── Database/
└── Scripts/
├── CreateTable.sql # schema
├── SeedData.sql # reference + demo data
├── Views.sql # read-side views
└── indexes.sql # performance indexes
- .NET 8 SDK
- Node.js 18+ (for Vite / Vue 3)
- SQL Server (local instance or container)
Run the scripts in Database/Scripts against your SQL Server instance, in order:
CreateTable.sql → Views.sql → indexes.sql → SeedData.sql
This creates the schema, read-side views, indexes, and seeds reference data (roles, specialties, statuses, urgency levels, shift blocks) plus demo records.
cd Backend/Referral-Management.Api/Referral-Management.Api
dotnet restore
dotnet runThe API starts with Swagger UI at /swagger. If you prefer EF migrations over the raw scripts, dotnet ef database update applies the baseline migration instead.
cd Frontend
npm install
npm run devThe SPA runs on http://localhost:5173 (the origin whitelisted in the backend CORS policy).
Backend settings live in appsettings.json. Secrets should be kept out of source control — set them via .NET user-secrets or environment variables for anything beyond local development:
The frontend reads the API base URL from its Axios configuration in src/api/axios.ts.
{ "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=Referral-Management;Trusted_Connection=True;TrustServerCertificate=True;" }, "Jwt": { "Issuer": "ReferralManagementAPI", "Audience": "ReferralManagementClient", "ExpiryMinutes": 360 // "Key": "<signing key — supply via secrets/env, not committed>" } }