Inspiration

We've all seen the posts: "URGENT: O+ blood needed at St. Mary's Hospital. Please share." forwarded through WhatsApp groups until the thread is three days old and the patient has already been moved. This is how blood emergencies are communicated in most cities, not through any system, but through luck and social reach.

The gap isn't willingness. In Nigeria alone, blood donation rates are critically low partly because the infrastructure for matching willing donors to urgent needs simply doesn't exist in real time. A donor two kilometres from a hospital might never know someone there needed their blood type today.

LifeLine is built from that specific failure: the problem isn't that donors are absent, it's that they're invisible. I wanted to build the layer that makes them findable, instantly, the moment a request goes up.

What it does

LifeLine is a real-time blood donor matching platform that closes the gap between emergency requests and nearby compatible donors.

  1. Donors register once with their blood type and location, a less than 30-second form, no account creation.
  2. Anyone can post a request ; hospital staff or family, specifying the blood type, urgency, and location. The matching engine computes exactly which blood types can donate safely using a shared compatibility matrix, then runs a geospatial query against registered donors using MongoDB's $geoNear operator, starting within 5 km and expanding to 15 km then 50 km until enough compatible donors are reached.
  3. Matched donors receive a live alert on their dashboard if they are active; no refresh, no polling or a push notification if they are away from the webapp.
  4. One tap to respond: a donor clicks "I can help" and both sides get each other's contact details instantly. The response is handled with an atomic database write that prevents race conditions when multiple donors reply simultaneously.
  5. Requests self-expire — critical requests disappear after 6 hours, urgent after 12, standard after 24 — via MongoDB's TTL index, so the board never shows stale emergencies. They can also be cancelled by the requester after if the deal has been sealed between both parties.

How we built it

LifeLine is an npm monorepo with three packages: an Express/Node.js API, a React/Vite frontend, and a shared domain package that both consume.

The shared package is the backbone of the monorepo structure — it exports the blood compatibility matrix and a Haversine distance function used by both the API (for matching) and the frontend (for client-side distance display on incoming alerts). This means the compatibility logic lives in one place and can never drift between layers.

  • Database: MongoDB Atlas with two collections: donors and requests each with a 2dsphere index on their location field. The requests collection also carries a TTL index on expiresAt, letting MongoDB itself clean up expired emergencies without any cron job.
  • Matching engine: A $geoNear aggregation pipeline filtered by compatible blood types and donor availability, with an escalating radius strategy. The query widens itself if fewer than five donors are found in the initial radius.
  • Real-time layer: Socket.io rooms keyed by blood type (donors:B+, donors:O-, etc.) so the server can broadcast a new request to exactly the right subset of connected donors in one targeted emit, without iterating over every connected socket.
  • Atomic response guard: The "I can help" endpoint uses a findOneAndUpdate with conditions on both status: "open" and the donor not already being in the responders array — a single round-trip that prevents double-responses and race conditions without optimistic locking.
  • Deployment: API on Vercel, frontend on Vercel, database on MongoDB Atlas M0 free tier.

Challenges we ran into

  1. Duplicate donors: every registration created a new account. Fixed with a find-or-create pattern and a unique index on phone number.
  2. Anonymous responders on page refresh: donor names vanished after page reload because only the ID was stored. Fixed by snapshotting name, phone, and blood type into the responder record at response time.
  3. Stale request sessions: requesters lost their status page on refresh with no way back. Fixed with localStorage persistence and a phone-based request lookup as a fallback.

Accomplishments that we're proud of

  1. Zero-friction real-time matching: a request goes up and compatible donors are alerted within a second, no app install or account needed on the requester's side.
  2. Search that thinks for itself: the matching radius expands automatically from 5 km to 50 km until enough donors are reached, with no manual intervention.
  3. Requests that clean up after themselves: every request carries an expiry, disappears from all dashboards when fulfilled or timed out, and leaves no stale emergencies for donors to chase.

What we learned

  1. Snapshot critical data at write time: relying on joins or lookups for live UI data breaks on refresh. Storing donor details directly in the responder record taught us to denormalize intentionally when reads need to be fast and self-contained.
  2. Session persistence is a UX feature, not an afterthought: users close tabs mid-flow. Designing for re-entry from the start (localStorage, phone-based lookup) is as important as designing the happy path.
  3. Real-time and REST are complementary, not competing: using REST for initial state and Socket.io only for live updates kept the architecture clean and made the app resilient when the socket connection dropped.

What's next for Lifeline

  1. Donor availability scheduling: let donors set hours they're reachable instead of a simple on/off toggle, reducing false alerts.
  2. Hospital dashboard: a dedicated view for medical staff to manage multiple active requests, track responders, and see fulfillment history in one place.
Share this project:

Updates