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.