How We Built Carivio: a Dispatch Platform for 50 Cars from Scratch
A three-platform taxi dispatching system in 4 months. 128,000 lines of backend, real-time ride auctions via SignalR, PostGIS geospatial, production Claude AI. What was hard and what you never see in a demo.
Carivio is a taxi dispatching platform we built for Transport Prague. Around 50 cars run on it today. It is our own product — backend, web app for dispatchers, and mobile app for drivers, built from scratch.
This article is about the architectural decisions we made and the things that are hard in production but invisible in a demo.
What It Is and What It Has to Do
Taxi dispatching looks simple from the outside: a customer books a ride, a driver accepts it, delivers the customer. In production it is a state machine with dozens of transitions, six user roles, and a requirement that every action happens in real time.
The platform has 6 roles: customer, driver, dispatcher, porter (hotel receptionist), admin, and guest. Each role sees a different view, has different permissions, and gets different notifications. There is one backend, but 48 feature modules. EF Core migrations grew to 107 — a good indicator of how much the domain model evolves over the course of a project.
The Auction Model for Rides
The biggest architectural decision was how to assign a ride to a driver.
We chose an auction model: when an order comes in, the system broadcasts it to a group of nearby drivers. They see the offer on their phone and can accept it. Whoever accepts first (or on the best terms) gets the ride. The offer expires for the rest.
The alternative — direct assignment to the nearest driver — is technically simpler. But it requires either a fixed rule that may not fit every situation, or a dispatcher deciding manually. Auctions give drivers autonomy and work well when someone is formally closest but has reasons not to be interested.
Technically that means: a state machine on the backend, SignalR broadcast of the offer to all relevant drivers, push notifications for when the app is in the background, and a Quartz job that watches the timeout and closes the offer when it expires. The state of every ride is persisted — if the server restarts mid-auction, the system survives it.
Real-Time: Four SignalR Hubs
Real-time communication runs over SignalR. We have four hub endpoints — one for customers, one for drivers, one for dispatchers, and one for porters.
Each hub has different groups, different events, and different latency requirements. The dispatcher sees all vehicle positions on the map in real time. The customer gets ride status notifications. The driver receives offers and updates.
You cannot just build this and forget it. Every time the state model changes, you have to go through all hubs and verify that every handler gets the right data at the right moment. Reference discovery across the codebase is a necessity during development, not a luxury.
Geospatial: PostGIS
We store driver GPS positions in PostgreSQL with the PostGIS extension. PostGIS provides spatial indexes and geographic queries in SQL — "find all drivers within 5 km of point X" is one query, not application logic.
For geocoding (address → coordinates) and place search we use an external API with a HybridCache + Redis layer. Geocoding results are cached — we query the same address once, not hundreds of times a day.
The backend also carries its own billing and invoicing engine. Taxi operations have specific invoicing requirements that off-the-shelf solutions do not cover.
What Is Invisible in the Demo: Background Location
The most complicated part of the entire platform is not in the backend logic. It is in the mobile app on the Android side.
Background location — sending the GPS position to the server even when the app is hidden in the background — looks simple on paper. In production it is a fight with device manufacturers.
Samsung, Xiaomi/MIUI, OnePlus, Huawei — each has its own aggressive battery optimisation that kills background apps. OEM manufacturers call "recommended behaviour" what is in effect a killed background process. The result: the driver thinks they are online, but the GPS position stopped arriving 10 minutes ago.
We handled it on several levels:
- OEM environment detection at app startup and a setup guide for the specific manufacturer
- Foreground service with a persistent notification — on Android the only reliable way to survive battery kill
- Detection of app termination and recovery on next launch (incomplete ride states, restoring SignalR connection)
- Offline queue: if the phone has no signal at the moment, GPS points are stored locally and sent when the connection is restored
This does not sound like a big project, but tuning behaviour on real devices from different manufacturers took a substantial share of mobile development time.
Production AI: Free-Form Input → Structured Order
One feature running in the system is free-text order processing via Claude.
A dispatcher or admin types anything into the field — "tomorrow morning at 8 from Wenceslas Square to the airport, 3 people, sedan" — and the model returns a structured object: vehicle type, number of passengers, pickup location and destination, time, notes.
The implementation has cost tracking on the backend (every request is logged with token counts) and server-side guardrails: the model receives a schema it must fill, and if it returns an incomplete or inconsistent output, the system rejects it and asks for a correction. Hallucinated addresses are a real problem — the dispatcher must be able to review and fix the output before the order goes to a driver.
Numbers
- Backend: ~128,000 lines of code
- Feature modules: 48
- EF Core migrations: 107
- SignalR hub endpoints: 4
- Quartz background jobs: 12 (ride state checks, auction timeouts, reminders, offline driver cleanup)
- Active vehicles in production: ~50
- MVP development time: 4 months
What to Take Away
MVP in 4 months was possible because the scope was clear from the start and architectural decisions were made in the first week, not on the fly. Auction vs. direct assignment, SignalR vs. polling, PostGIS vs. application-side geospatial — these are decisions that are expensive to change late.
The most costly part of development was not complex business logic. It was tuning background location on real phones from different manufacturers and handling states that never appear in a demo but happen every day in production.
Not every project moves at this pace. It depends on how much changes on the fly and how much the domain needs to be explored. Carivio had the advantage that the client knew exactly what they wanted.
If you are building your own dispatching system or thinking about fleet digitalisation, reach out — we are happy to share what we would do differently and what makes sense for your case.
FAQ
How long did it take to build Carivio from scratch?
MVP in 4 months. The condition was a clear scope and architectural decisions made upfront, not on the fly. Not every project moves that fast — it depends on domain complexity and how much changes during development.
Why did you choose the auction model for rides instead of direct assignment?
Direct assignment requires either a fixed rule (nearest driver) or a dispatcher making manual decisions. Auctions give drivers autonomy and naturally handle situations where someone is technically closest but not interested. Technically it means a state machine and SignalR broadcast — more complex than a simple assign, but more flexible in operation.
Can the platform handle a fleet larger than 50 cars?
The architecture is ready for it. PostGIS spatial indexes, HybridCache with Redis, and a stateless backend allow horizontal scaling. Actual capacity depends on traffic and infrastructure — that would need a load test with real numbers.