All Projects
BloodOS Banner

BloodOS

A modern, full-stack blood donation coordination platform designed specifically for Bangladesh. Features real-time blood request management, intelligent donor matching, and comprehensive admin controls.

Next.js 16React 19TypeScriptExpress.js 5MongoDBRedisBetter AuthTailwind CSS v4shadcn/ui v4ZodVitest
RoleFull-Stack Engineer
CategoryHealth Tech & Donor Coordination
Timeline2026
StatusLive Product

Overview

BloodOS is a comprehensive blood donor coordination platform I built to address the critical need for efficient blood donation management in Bangladesh. During medical emergencies, patients and families often struggle to find compatible blood donors quickly, resorting to social media posts or phone calls that waste precious time. I wanted to create a centralized, reliable platform that connects people who need blood with verified donors in their area instantly. The platform features a modern, mobile-first interface built with Next.js 16 and React 19, powered by a robust Express.js backend with MongoDB. Users can browse active blood requests, filter by blood group and location, create urgent requests, and manage their donor profiles. The system includes intelligent eligibility tracking (90-day donation rule), automated request expiration, phone number privacy masking, and a sophisticated state machine for request lifecycle management. I implemented both frontend and backend as a monorepo project, with comprehensive authentication via Better Auth, role-based access control, an admin dashboard for platform moderation, and optional Redis caching for improved performance. The entire stack is type-safe with TypeScript, validated with Zod schemas, and includes service-level tests for critical business logic.

The Problem

In Bangladesh, finding blood donors during emergencies is a chaotic, time-consuming process. Patients' families often post desperate appeals on Facebook groups, make countless phone calls, or visit hospitals hoping to find compatible donors. There's no centralized database of willing donors, no way to verify donor eligibility (when they last donated), and no privacy protection for phone numbers. This fragmented system costs precious time during medical emergencies. I wanted to solve this by creating a reliable, searchable platform where donors can register once and be found instantly by anyone who needs their blood type, with built-in eligibility checks and privacy protections.

The Solution

I built BloodOS as a full-stack TypeScript application with clear separation between frontend (Next.js) and backend (Express.js). The frontend uses shadcn/ui v4 components with Tailwind CSS v4 for a modern, accessible interface that works seamlessly on mobile devices. I implemented Better Auth for session-based authentication, with the backend validating sessions by calling the frontend's auth endpoint—this ensures both apps must run together but provides a clean separation of concerns. On the backend, I designed a state machine for request lifecycle (Pending → Fulfilled/Cancelled/Expired), created services for donor eligibility calculation based on the 90-day rule, and implemented phone number masking (01XXX***XXX) for privacy. I used MongoDB native driver (no Mongoose) with carefully designed indexes for performance, Zod for runtime validation, and added optional Redis caching that gracefully degrades if unavailable. The admin dashboard gives moderators full control over users and requests. I also wrote service-level tests using Vitest to ensure the business logic for compatibility checking, eligibility, and state transitions works correctly.

Architecture & Engineering

The architecture is a TypeScript monorepo with two separate applications. The frontend is built on Next.js 16 with React 19, using App Router for file-based routing with route groups: (public) for landing/auth pages, (protected) for authenticated features, and (admin) for admin-only pages. Route protection is handled via proxy.ts (Next.js 16's middleware replacement), and I use shadcn/ui v4 base-nova components built on @base-ui/react (not Radix UI). Tailwind CSS v4 is configured entirely through CSS @theme directives in globals.css—no tailwind.config.ts. The backend is Express.js 5 with TypeScript ESM (module:nodenext), using MongoDB native driver for data persistence and optional Redis for caching. Authentication uses Better Auth with a unique architecture: the backend doesn't verify JWTs locally—instead, auth.middleware.ts calls the frontend's GET /api/auth/get-session endpoint with request cookies to validate sessions. This means both apps must run for authenticated API calls to work. I implemented comprehensive endpoints for requests, donors, notifications, donations, stats, admin functions, and file uploads (avatar via IMGBB API). The server has strict TypeScript config (exactOptionalPropertyTypes, noUncheckedIndexedAccess) and uses Zod schemas for all validation. Express 5's built-in async error handling eliminates the need for try/catch wrappers.

Technical Challenges

  • Authentication architecture - Designing the Better Auth integration where the backend validates sessions by calling the frontend's auth endpoint was unconventional. I had to ensure both apps run together and handle the cross-service communication properly, including cookie forwarding and error handling when one service is down.
  • MongoDB indexing strategy - With no Mongoose, I had to manually design compound indexes for complex queries (filtering by blood group + district + urgency + status). I created scripts to initialize and verify indexes, and wrote aggregation pipelines for statistics. Getting the index order right for optimal query performance took several iterations.
  • Request state machine - Implementing a robust state machine for request lifecycle (Pending → Fulfilled/Cancelled/Expired) with validation to prevent invalid transitions was challenging. I had to handle automatic expiration based on needed-by dates, ensure only request owners could update status, and write comprehensive tests to verify all transition rules.
  • Phone number privacy masking - Implementing consistent phone number masking (01XXX***XXX format) across the API while ensuring the full number is still stored for actual contact was tricky. I had to create utility functions that mask on read but preserve the original data, and ensure the masking works in all response contexts (requests, donor profiles, notifications).
  • Tailwind CSS v4 configuration - Learning the new @theme directive approach instead of tailwind.config.ts was a paradigm shift. I had to configure everything via CSS custom properties in globals.css, use oklch colors exclusively, and adapt shadcn components to work with this setup while maintaining type safety.
  • Type-safe MongoDB queries - Without Mongoose, maintaining type safety with the native MongoDB driver required creating typed collection getter functions in db/collections.ts. The strictest TypeScript settings (exactOptionalPropertyTypes, noUncheckedIndexedAccess) caught many potential runtime errors but made indexed access more verbose.

What I Learned

  • Strict TypeScript settings like exactOptionalPropertyTypes and noUncheckedIndexedAccess catch real bugs early but require more careful handling of optional fields and array access. I learned to embrace the verbosity for the safety it provides.
  • Express 5's built-in async error handling is a game-changer—no more express-async-errors or manual try/catch wrappers around async route handlers. Thrown errors automatically get caught and passed to error middleware.
  • MongoDB native driver is more verbose than Mongoose but gives you complete control over queries and better performance. Designing proper indexes and understanding aggregation pipelines is crucial for scalability.
  • Better Auth's flexible architecture allows creative session validation strategies. The frontend-validates-backend approach I used is unconventional but works well for a monorepo where you control both sides.
  • shadcn/ui v4 with @base-ui/react (base-nova variant) provides fully accessible components without Radix UI's bundle size. The new Tailwind v4 configuration via @theme directives is cleaner once you understand it.
  • Writing service-level tests for business logic (eligibility, compatibility, state machine) is far more valuable than integration tests for CRUD operations. Vitest with globals enabled makes testing ergonomic.
  • Building for a specific region (Bangladesh) requires cultural considerations: phone number formats (11-digit starting with 01), district names in Bengali/English, and understanding local blood donation practices (90-day rule).
  • Optional dependencies like Redis should degrade gracefully. I learned to write services that check for Redis availability and fall back to direct database queries when caching isn't available, making the system more resilient.