Sendit GH Backend — Monolith but in Modules
TL;DR (non‑technical)
- One backend serves the client, vendor, and rider apps.
- Inside, it’s neatly organized into modules (auth, events, restaurants, delivery).
- This keeps costs and complexity low while making changes fast.
- If a module needs its own resources later, we can split it cleanly because boundaries already exist.
What the backend does
- Client app: browse/select services, create orders/shipments, pay, track.
- Vendor app: publish/organize (events/restaurants), manage orders, reconcile.
- Rider app: accept/complete deliveries with proof-of-delivery and availability.
Architecture (as built)
- Style: Modular monolith with explicit module exports and domain layering.
- App contexts: Multi-app auth that distinguishes client/vendor/rider via app-specific routes and claims.
- In‑process boundaries: Modules communicate via function calls and domain events, not network hops.
- Adapters at the edge: DB (MongoDB via Mongoose), cache (Redis), queue (Bull), file storage (local or Cloudinary), SMS (Arkesel), email (Nodemailer).
Technology stack (from the code)
- Runtime: Node.js 20+, TypeScript 5+
- Framework: Express 5
- Database: MongoDB 6+ via Mongoose
- Cache: Redis 7+ via ioredis
- Queue/Jobs: Bull
- Auth: JWT (multi‑app context: client, vendor, rider)
- Validation: Joi
- File storage: Local filesystem or Cloudinary (env‑switchable)
- SMS and Email: Arkesel (SMS), Nodemailer (email)
- Logging: Winston, HTTP logging with Morgan
- API Docs: Swagger (swagger-jsdoc, swagger-ui-express)
- Testing/Tooling: Jest, ESLint, Prettier, ts-node-dev
Modules (current)
- Auth: sessions, app-context auth, RBAC hooks.
- Events: ticketing module (marked complete in structure).
- Restaurants: catalog/menu/orders domain.
- Delivery: parcel lifecycle and rider assignment.
Infrastructure and operations
- Edge/proxy: App trusts a managed proxy (e.g., DigitalOcean App Platform) for client IPs and TLS termination.
- API gateway: Express with rate limiting hooks, security headers (Helmet), CORS, and request IDs.
- Health and introspection: health endpoint (DB, Redis, queues), route list/validate, Swagger docs.
- Observability: structured logs with request IDs and module tagging; queue stats reported in health.
- Secrets and config: environment‑based configuration (dotenv); per‑env storage (local vs Cloudinary).
- Background processing: Bull queues for async work (notifications, fan‑out).
- Static files: serves /uploads for local dev storage.
Data and performance
- Data layer: single MongoDB; collections logically grouped by module (prefixing).
- Caching: Redis for sessions and hot reads; rate limiting middleware scaffolded.
- Hot paths: synchronous in‑process flows for quoting/order creation and auth.
- Async where needed: notifications, reconciliation, and webhook fan‑outs via Bull.
- API design: REST endpoints by domain; idempotent handlers for critical flows (supported by route registry and validation patterns).
Security and privacy
- Token-based auth with app context (vendor/client/rider).
- Role-based access checks in middleware and services.
- Input validation (Joi), security headers (Helmet), and CORS.
- PII protection via TLS at the edge and scoped access in services.
Why a modular monolith here
- Lower ops tax: one deployable, one log stream, predictable debugging.
- Faster iteration: vertical slices per module; cohesive changes across controllers/services/models.
- Clean seams: module contracts make later extraction safer if scale/SLA truly requires it.
What being a solo developer changed
- Lean setup: shared services (auth, queue, file) reused across modules.
- Tests where it matters: domain/service level fast tests; contract testing for adapters.
- Operability first: health, logs, and docs landed early to keep the system “boringly” operable.
Roadmap for when to split
- Independent scaling or strict SLAs (e.g., payments/compliance).
- Compute/runtime divergence (e.g., real-time location streaming).
- Clear team ownership mapping to a boundary.
Explore other projects
Ancient Chat: Building and Running a Fast-Growing Messaging Platform
Ancient Chat is a private messaging app for Android and iOS: one-on-one and group chats, voice and video calls, 24-hour stories, communities, and in-app gifting. In about five months it grew from under 10,000 users to well over 100,000 , with tens of thousands of people using it every day , and we're building it to handle a million. Two of us built and run the whole thing, from the Flutter app to the servers it lives on. In mid-2025 we rebuilt the app from scratch and relaunched it in late 2025. This is how it's put together and the parts I owned. The Stack The app talks to a self-hosted Supabase stack (Postgres, Realtime, Auth, Storage, PostgREST and a connection pooler) behind nginx, instead of Supabase's hosted cloud. Self-hosting keeps costs predictable at our scale and gives us full control of the database. Around it sit a few small, single-purpose services: Media processor (Node.js + FFmpeg): transcodes chat and story videos, images and stickers, and generates thumbnails. Moderation service (Python): scans uploaded images and videos and sends borderline cases to a human review queue. Notification server (Node.js): receives database webhooks on new messages and sends push notifications through Firebase Cloud Messaging. OTP delivery (Deno): sends login codes over WhatsApp first, with SMS as a fallback (Hubtel in Ghana, Twilio elsewhere), plus Redis-based rate limits to stop abuse. Calls : a signaling service in front of a LiveKit media server for voice and video. Offline-First Messaging A chat app has to open instantly, even on a weak connection. I led the move to an offline-first design: the phone keeps its own SQLite database of chats, messages, reactions and read receipts, and the app always reads from that first. Supabase Realtime events are applied as small changes to the local database instead of triggering a full refetch, and sync only fetches what changed since the last update. Media is cached on the device with size budgets per type (avatars, thumbnails, full media), so the app stays fast without filling up storage. The Media Pipeline I designed the media processor and the thinking behind it. Chats and stories get different quality profiles: chat video is kept sharper, and story video is lighter. Video bitrate scales with clip length, so short clips stay crisp and long ones stay small. Before re-encoding, the worker checks whether the phone already produced a file that fits the profile, and if so leaves it alone. That avoids a second lossy encode and saves server time. The same rules live in the app and the worker, and tests on both sides fail if they drift apart. Running It in Production We run everything on our own servers. As usage grew we went through several server migrations, most recently moving Postgres onto a dedicated database server so it no longer competes with the API, realtime and storage services for CPU, memory and disk. Services run under pm2, deploys go through GitHub Actions, and each host has its own monitoring agent. What I Owned Large parts of the Flutter app, alongside one other engineer The offline-first sync and caching design The media processing pipeline and the moderation service, end to end Push notifications and OTP delivery The self-hosted infrastructure and the server migrations Lessons Local first beats fast servers. Reading from the device and syncing in the background did more for perceived speed than any backend tuning. Small services with one job are easy to reason about. Each one can be deployed, restarted and debugged on its own. Self-hosting is a real job. It saves money, but you own backups, monitoring and migrations. Write down every change to the servers.
Building RideMe: A High-Performance Ride-Hailing Backend with NestJS
RideMe is a ride-hailing platform backend I built with NestJS and TypeScript. It supports 5,000 concurrent users with sub-200ms latency, real-time driver matching, and a modular architecture. The Challenge The goal was a backend that could: Handle 5,000 concurrent active riders Maintain P95 latency under 200ms Support real-time driver matching and location tracking Process payments securely Scale horizontally without major refactoring Remain cost-effective and self-hostable Architecture & Technology Stack The system uses a monolithic-but-modular NestJS architecture with clear module boundaries. Core Technologies: NestJS 10+ : TypeScript framework with dependency injection and modular design PostgreSQL 15+ with PostGIS : Geospatial queries for driver location and route calculations Prisma ORM : Type-safe database access with strong migration tooling Redis 7+ : Caching and session management with geohash support for nearby driver queries BullMQ : Background job processing for trip matching and notifications WebSocket Gateway : Real-time updates using Socket.io with Redis adapter for horizontal scaling Why This Stack? PostgreSQL with PostGIS provides native geospatial operations (distance calculations, contains queries) without external services. Prisma offers type safety and solid migration workflows. Redis handles caching, sessions, and geohash indexes for driver locations. BullMQ provides retry logic and priority queues for critical operations like trip matching. Modular Architecture The backend is organized into focused modules: Auth Module : OTP-based registration, JWT authentication with access/refresh tokens, password reset flows, and role-based access control (user, driver, admin). Trip Module : Trip lifecycle management from creation to completion, multi-stop trip support, driver matching algorithm, trip cancellation with retry logic, and rating/review system. Geolocation Module : Geo data storage and retrieval, driver location tracking with debounced updates, nearby driver queries using geohash, address geocoding via Google Maps, and route calculation with polyline generation. Payment Module : Integration with Paystack and Stripe, payment status tracking, refund processing, driver payout management, and transaction history. Driver Module : Profile management, vehicle registration, license management, availability status tracking, and earnings/payout tracking. Notification Module : Push notifications via Firebase Cloud Messaging, notification templates, delivery status tracking, and bulk notifications. Admin Module : Dashboard analytics, user/driver management, trip oversight, and system configuration. Performance Optimizations Database Optimizations: Composite indexes on common query patterns ( [status, createdAt] for trips, [driverId, status] for driver queries) N+1 query prevention using Prisma's include and batch queries Query result caching with appropriate TTLs (1 hour for profiles, 5 minutes for trip estimates) Caching Strategy: Redis cache-aside pattern with 80%+ hit rate target Geohash-based driver location caching (30-second TTL) Static data caching (vehicle makes/models: 24-hour TTL) Dashboard analytics caching (5-minute TTL) Response Optimizations: Gzip compression reducing response sizes by 60-80% Data minimization using Prisma select to return only needed fields Pagination for all list endpoints Performance interceptor logging slow requests (>1000ms) Load Testing: Comprehensive k6 load tests validate: 500 RPS sustained load 1000 RPS peak load 5000 concurrent users All scenarios maintain P95 latency < 200ms Real-Time Driver Matching The matching system uses: Geohash Indexing : Driver locations stored in Redis using geohash for efficient nearby queries Debounced Updates : Location updates debounced (30 seconds or >100m movement) to reduce database writes Priority Queue : Trip matching jobs processed with high priority in BullMQ WebSocket Broadcasting : Real-time trip assignments broadcast to nearby drivers Fallback Mechanisms : Retry logic if no drivers accept within timeout Security Architecture Authentication & Authorization: JWT tokens with 15-minute access tokens and 7-day refresh tokens Separate admin authentication system Role-based guards (JwtAuthGuard, AdminAuthGuard, AdminRoleGuard) Password hashing with bcrypt (cost factor 10) API Security: Input validation using class-validator on all DTOs SQL injection prevention via Prisma's parameterized queries Rate limiting (configurable per endpoint) CORS configuration for production domains Security headers (X-Frame-Options, CSP, HSTS) Data Protection: Passwords never stored in plain text or returned in responses Sensitive data excluded from queries using Prisma select GDPR compliance with user deletion request tracking PCI DSS compliance via secure payment providers (no card storage) Migration from PHP/Laravel The backend was rebuilt from a PHP/Laravel implementation with: API Compatibility : Maintained endpoint compatibility for seamless frontend integration Dual-Write Strategy : Parallel writes during migration phase for data consistency Gradual Rollout : Incremental traffic shifting with easy rollback capability Zero Downtime : Users experienced no service interruption during migration Developer Experience Type Safety : Full TypeScript coverage with Prisma-generated types ensuring compile-time safety. Testing : Comprehensive test suite with Jest for unit tests and Supertest for E2E tests, achieving high coverage. Documentation : Swagger UI at /api-docs , comprehensive architecture docs, API mapping guides, and deployment documentation. Code Quality : ESLint + Prettier, Husky pre-commit hooks, and structured logging with Pino. Scalability Considerations Horizontal Scaling: Stateless API instances sharing Redis for sessions/cache WebSocket scaling via Redis adapter enabling multi-instance support Separate worker processes for background jobs Database read replicas for analytics queries Future Microservices Path: The modular architecture allows extraction into microservices if needed: Trip Service (trip module + matching logic) Driver Service (driver module + location tracking) Payment Service (payment module + provider integrations) Notification Service (notification module + FCM) Results & Impact The new backend delivers: Performance : P95 latency consistently under 200ms under load Scalability : Handles 5,000+ concurrent users with room to grow Reliability : Comprehensive error handling, retry logic, and failure-safe flows Maintainability : Clean module boundaries, type safety, and comprehensive documentation Cost Efficiency : Self-hostable stack using free/open-source technologies Lessons Learned Geospatial Queries : PostGIS is essential for location-based applications—native distance calculations outperform application-level solutions. Caching Strategy : Appropriate TTLs and cache invalidation are critical—80%+ hit rates significantly reduce database load. Type Safety : TypeScript + Prisma catches errors at compile time, reducing production bugs. Modular Design : Clear module boundaries make the codebase maintainable and enable future microservices extraction. Load Testing Early : Identifying bottlenecks before production prevents performance issues. Conclusion The RideMe backend demonstrates that modern TypeScript frameworks like NestJS can deliver high-performance, scalable systems. By combining PostgreSQL's geospatial capabilities, Redis for caching, and a modular architecture, we built a system that handles thousands of concurrent users while maintaining sub-200ms latency. The architecture prioritizes developer experience, maintainability, and cost-effectiveness—proving that you don't need expensive cloud services to build production-grade ride-hailing infrastructure. The modular design ensures the system can evolve as requirements grow, whether that means adding features or extracting microservices. For developers building similar real-time, location-based applications, the key takeaways are: invest in proper geospatial database support, implement aggressive caching strategies, prioritize type safety, and design for horizontal scaling from day one. Built with NestJS, PostgreSQL, Redis, and TypeScript. Open to questions and discussions about architecture decisions, performance optimizations, or implementation details.
Property Pulse GH. An AI-Powered Property Listing App
I built the frontend of Property Pulse GH , an AI-powered property listing platform that makes it easier for people in Ghana to find homes that match their needs. Using Next.js 15 with the App Router, I created a responsive interface where users can search, filter, and explore property listings. Technical Stack Built with Next.js 15 (App Router), React 19 , and TypeScript . Key technologies: Tailwind CSS v4, shadcn/ui, React Context API, custom hooks, Axios service layer, React Hook Form with Zod, NextAuth.js, React Leaflet, Framer Motion, and real-time notifications. Core Features Advanced Search & Filtering : Multi-criteria filtering (price, type, bedrooms, location), real-time search, grid/list/map views. AI Recommendations : Personalized property matching based on user preferences and behavioral insights. Property Management : Full-screen image galleries with zoom, interactive property cards, detailed pages with maps, saved properties. Multi-Role Dashboards : Client dashboard (recommendations, saved properties), Agent portal (property management, photography booking, subscriptions, earnings), Photographer dashboard (bookings, sessions, analytics). Subscriptions & Payments : Multi-tier plans, Hubtel integration for GHS, usage tracking, team management. Photography Services : Booking system with multiple service types, dynamic pricing, photographer ratings. Real-Time Notifications : Multi-channel updates (email, push, SMS) for contacts, bookings, payments. Performance & UX Optimizations : Next.js Image optimization, lazy loading, Turbopack builds, code splitting, SEO with Metadata API. User Experience : Mobile-first responsive design, accessibility (ARIA, keyboard navigation), custom dark/light theme, multi-currency support with GHS formatting. Backend Integration Since the client handled the backend, we ensured smooth integration through centralized API service layer, full TypeScript type safety, consistent error handling, JWT authentication with auto-refresh, and real-time data synchronization. Technical Solutions State Management : Layered approach with React Context, custom hooks, and local state. Image Performance : Lazy loading, thumbnail navigation, modal viewer with keyboard controls. Real-Time Updates : Notification context with polling and reactive state management. Currency Support : Context provider for exchange rates, formatting, and conversion. Result The platform combines AI recommendations with a seamless browsing experience. Users can discover properties through intelligent recommendations, search and filter effectively, save favorites, connect with agents, access photography services, and manage subscriptions with local payment integration. The frontend is scalable, maintainable, and optimized for performance, accessibility, and user experience.