Mova Mobility moves people around Brazzaville and beyond. In the local terms that matter, country code +242, currency the Central African CFA franc (FCFA / XAF), it is not an abstract “ride-hailing” clone. It is three distinct transport products that happen to share one brand, one backend and one account system:
Group charter for events. You book a minibus with a driver for a wedding, a funeral, a school trip or company travel. You pick the event, the route and its stops, the date, and how many vehicles (a 15-seat Toyota Hiace, a 30-seat Coaster).
The app quotes a price; Mova confirms and invoices. This is the core, and it is deliberately group transport for occasions, not solo trips across town.Fleets for companies. The same machinery, sold to businesses for staff transport and seminars, quotes, per-trip tracking, the lot.
Mova Pass. A daily commuting subscription that lives on an NFC card. You tap it at the gate; a fare inspector’s phone reads it and says, in a fraction of a second and with no network, whether it is valid. Forgot your card? A QR fallback (Pass Éclair) covers you.
Three products, one company. That single fact, that these are different
jobs-to-be-done under one roof, is the seed of nearly every architecture decision that follows.
The shape of the system: one backend, five repositories
The whole platform is five repositories, one of which is the brain:
Repo | What it is |
|---|---|
| The backend, a pure JSON API, no UI of its own |
| The passenger app (charter and Mova Pass) |
| The back office |
| The fare-inspector field app |
| mova-mobility.com marketing, jobs, contact |
One API serves four clients. A passenger quote on a phone, a refund in the back office, a Pass check at a gate, a contact form on the website every dynamic request lands on the same Laravel origin. That is a bet, not an accident: for a small team, one backend to reason about, deploy and monitor beats a constellation of services, and a backend change can reach every client in a single deploy with no app-store round
trip. The cost, a single origin, a single blast radius, is one we take knowingly.
The three hard problems, and how each was solved
A project is defined by its hard parts. Mova has three that were genuinely difficult, and each produced a decision worth keeping.
Verifying a transit pass with no signal. A fare inspector at a gate cannot wait on a network round trip, and often has no signal at all. So the Mova Pass is verified offline: each card carries a cryptographically signed payload, and the inspector’s app checks that signature on-device with no server call. The signing is asymmetric (Ed25519), which means the key that verifies a card can ship inside a public app safely, it can confirm a signature but never forge one. The honest limitation, stated rather than hidden: a static card can be cloned, and the real fix (a dynamic-authentication chip) is a known upgrade path for after the pilot.
Making a closed phone ring. In-app calling (a passenger and an operator talking) has to work when the app is closed, the phone must ring in your pocket. That is three operating-system rules deep. The calling SDK starts no background service of its own, so on Android a small native module supplies the microphone foreground service that keeps a call alive, and an incoming call is rung with a high-priority push and a full-screen notification rather than a service, because Android forbids starting a foreground service from the background, which is exactly the state an incoming call arrives in. On iOS it is PushKit and CallKit.

Handling money without a licence to hold it. Mova takes payments (MTN Mobile Money, Airtel Money, cards) and runs an in-app balance called Mova Credit. Holding customer money in the CEMAC region requires an e-money licence that very few companies have. So Mova Credit is closed-loop by construction: there is no way to put cash
into it as stored value and no way to take cash out, those methods simply do not exist in the code. The wallet is an append-only ledger of credits earned and spent inside the product, which keeps it the right side of the regulatory line without relying on anyone remembering a rule.

The backend, in brief
Underneath all three products sits a Laravel API built to be boring, cheap and reversible, the correct aesthetic for a team with no ops department.
Two kinds of user, kept apart by type. Passengers (
Client) and staff (User) are two separate models behind two separate guards, so a passenger token cannot reach a staff route, the separation is structural, not a role flag someone has to check correctly on every endpoint.Authorization as route-sets. A permission is literally a set of API routes; access is deny-by-default, and a test fails the build if any staff route is left unclassified. Roles carry constraints on time, network and country. So a new endpoint cannot ship silently open or silently forgotten.
Pricing that ops can change without a deploy. Trip prices are built by operators in the back office and published as versions; until the first version exists, a committed config file is the algorithm, and the first draft is that config. The margin, Mova’s commission and the operator’s payout, is computed on the server and never sent to a customer’s phone.
Edge and infrastructure. The API sits behind Cloudflare (WAF, caching, trusted proxies, R2 file storage); realtime runs on self-hosted Reverb websockets; push goes through Expo as a relay over FCM and APNs. The one place the system knowingly runs under spec, an undersized origin, is logged and monitored rather than denied.
The parts that do not show up in a demo
Some of the most consequential work is invisible when the app is running well, and it is where a platform earns trust.
Privacy by retention. A transport app knows where people are and when they move, which makes its telemetry one careless field away from a surveillance log. So Mova runs four separate observability systems, each holding only what its question needs, and every retention window is an argument rather than a default: a bus’s live location is kept 30 days, a Pass scan’s coordinates 90 days (the scan record survives, only the location is forgotten), audit entries 400 days for changes and 90 for reads. A single redactor stands in front of the audit log and erases secrets, reduces typed messages to their length, and masks identifiers to their last four digits.
Honesty about the product. The old privacy policy named a payment processor the company has never used; the old marketing site promised a metro network that does not exist. Both were artifacts that had drifted from reality, a template and some old ambition. Reconciling the site’s claims against what the code actually does is
unglamorous and important, because a privacy policy that names the wrong processor is a legal document that is false.
Shipping it: the store saga
The last mile, getting two native apps through Apple and Google review, was its own gauntlet, and most of it had nothing to do with code we wrote. A foreground-service permission dragged in by the audio library; another dragged in by the calling SDK; an NFC entitlement the newest iOS SDK rejects; a camera-usage string required because the
calling SDK links camera APIs the app never uses; Firebase config files that must match each app’s bundle exactly. And then the one no linter can fix: Apple rejected the app not for a defect, but to demand a video of the NFC feature working on a real device, because NFC cannot be exercised in the simulator and a reviewer cannot verify a physical-card tap from the binary alone. The lesson that ties the saga together: most
of what blocks a React Native app at the store is in the SDKs you pulled in and the entitlements they imply, not the code you authored. Audit the merged manifest, not just your source.

Find more here:



