← Back

SaaS product · Web + Mobile + Backend

Téranga – Nova-Core Mobility

Goal

Give Senegalese car-rental agencies a complete platform to manage their fleet, bookings, payments and customer relationship, instead of manual tools (notebooks, WhatsApp, untracked transfers).

Role

Founder of Nova-Core and lead developer: product scoping, architecture, backend, web and mobile development, deployment and production operations.

Impact

Platform in production for its first customer, Téranga SN Automobile: online bookings with document verification, mobile-money payment, automatically generated and electronically signed rental contracts, airport shuttle (AIBD) and vehicle sales. The admin dashboard is real time: a customer request shows up at the agency without reloading.

Key technologies

Spring Boot 3 / Java 21, MongoDB 7, Kafka, Angular 17, Flutter (Riverpod, GoRouter, Dio), MinIO, PayDunya, Africa’s Talking, SSE + FCM, STOMP/WebSocket, Nginx.

Visit the live site ↗

Description

Téranga is a multi-tenant SaaS: each agency (“parking”) has its own catalogue, versioned rental terms, contract branding and administrators, while sharing a single infrastructure. Customers book from the web or the mobile app; the agency runs everything from an admin dashboard.

One backend serves three channels:

  • Angular 17 web (standalone components) — customer area and agency dashboard, real time via SSE.
  • Flutter mobile (Android / iOS) — catalogue, booking, payment, shuttle, support chat, push notifications.
  • Spring Boot 3 backend — REST API, security, payments, PDF, notifications, business schedulers.

The booking flow is enforced by the backend, which remains the single authority: mandatory documents (driving licence + national ID both sides or passport), eligibility rules (age, licence seniority), acceptance of versioned rental terms, date-conflict checks, then payment within a limited window.

Customer

Téranga SN Automobile (Dakar) — first tenant in production

Technologies

  • Backend: Spring Boot 3.3, Java 21, MongoDB 7, Kafka, OpenPDF, Thymeleaf, Bucket4j
  • Web: Angular 17, RxJS (throttled event bus)
  • Mobile: Flutter, Riverpod, GoRouter, Dio, Firebase Messaging
  • Payment / SMS: PayDunya SOFTPAY (Wave, Orange Money), Africa’s Talking (OTP)
  • Infra: Linux, Nginx, systemd, MinIO, Sentry, Better Stack

Features & responsibilities

  • Authentication
    • Login by phone, username or email; sign-up by SMS OTP or by email (diaspora).
    • JWT with refresh-token rotation, httpOnly cookie on the web, Bearer on mobile.
    • Single session per user: any new login revokes the previous ones.
  • Bookings
    • Availability by time slot, ranges spanning a confirmed booking are blocked.
    • Auto-cancellation of expired requests and of conflicting requests on acceptance (schedulers).
    • Rental contract PDF generated once, with the agency’s signature and stamp.
  • Payment & notifications
    • Two-step Wave / Orange Money payment, signed IPN verification.
    • Real-time notifications (SSE) in the foreground, FCM push in the background, Thymeleaf emails.
    • Customer ↔ agency support chat over STOMP/WebSocket.
  • Other modules
    • AIBD airport shuttle with reviews, vehicle sales with deposit, favourites, document management.
    • Relationship-based access control (ReBAC): owner, employee, admin per agency.

Technical challenges solved

The mobile “wake-up” bug: a burst of errors every time the app came back to the foreground

Symptom: after the app had been in the background, the first requests failed, then everything worked a few seconds later. The usual suspects (sockets, Nginx keep-alive, timeouts) did not explain it.

Root cause: the backend authentication entry point returned a 401 body in invalid JSON (single quotes). On mobile, the HTTP client failed to decode it and surfaced a generic error with no status code: the interceptor never saw the 401 and never triggered the token refresh.

Fix: valid JSON on the server (a one-liner), plus a resume gate on mobile that holds private requests until the token has been refreshed. Rule adopted: an auth filter never writes a blocking response and always writes valid JSON.

HTTP connections contaminated by SSE errors

Symptom: intermittently, every API request received a 204 No Content instead of its response.

Root cause: SSE disconnections (broken pipe) were dispatched by Tomcat to /error, whose controller answered 204; on keep-alive connections between Nginx and Tomcat that response was served to unrelated requests.

Fix: /error always answers with valid JSON and Connection: close, and the SSE stream gets its own Nginx block (HTTP/1.0, no keep-alive); a 30-second heartbeat purges dead emitters.

Double acceptance of overlapping bookings

Symptom: an administrator could accept two partially overlapping bookings on the same vehicle.

Root cause: the conflict query only covered the case where an existing booking fully contained the new one, and ignored the PAID status.

Fix: one conflict query shared by creation and acceptance (correct overlap logic, ACCEPTED + PAID statuses), and on acceptance the other pending requests on the same period are auto-cancelled with a customer notification.

A real-time dashboard that holds under load

Problem: every SSE notification triggered a full dashboard reload; 50 requests in a few seconds meant 50 reloads.

Fix: an RxJS event bus throttled per event type (first reload immediate, then coalesced over 800 ms), automatic SSE reconnection with backoff and a polling fallback.

← Back to projects Next project: Vouchers →