New: How we build — modern AI tooling, strict guardrails, every line reviewed by a person. Read our engineering practices →New: How we build. AI tooling, strict guardrails, human review. Read more →

Mobility and passenger transport · Netherlands

Taxionspot: the audit that found the fixed price was not fixed

A licensed taxi operator in Eindhoven running airport, business, medical and school transport on one promise: a price you see before you book and pay after you ride. We were brought in partway through somebody else's build, audited what was there, and rewrote the platform on foundations that hold.

  • 1 party decides the price
  • 0 commercial decisions on a device
  • 2 platforms, one backend
  • 4 audit findings before a feature was written

What happened

We did not start this project. We were handed it partway through, part-built by another team, and asked to take it forward.

We audit before we build. That is standard practice on any inherited codebase, and the reason is simple: a team that starts adding features to an unexamined build inherits every decision in it without knowing a single one.

The audit found something that mattered more than the code. The business sells a fixed price. The software was not deciding that price. The number a customer saw was being produced on their own device and accepted by the server as fact, which means the central commercial commitment of the company was being set outside the company.

Nobody had noticed, because nobody had looked. That is the ordinary reason a problem like this survives. It does not announce itself, nothing errors, and the bookings that come through look exactly like the ones that should.

So we rewrote rather than continued. Not because rewriting is the exciting answer, but because the foundation could not carry what was going to be built on it, and every month of building anyway makes that more expensive rather than less.

Client
Taxionspot, a licensed taxi operator in Eindhoven, Netherlands
Sector
Mobility and passenger transport
Operation
Airport, business, group, medical and school transport, twenty-four hours
Our scope
Audit of an inherited build, then a rewrite across web and mobile covering booking, dispatch, fleet, payments and the platform underneath

The one fact that shaped everything

In a fixed-price business, the price is not a number. It is the product.

Everything else this operator sells is available elsewhere. A car, a driver, a route. What they sell that a metered competitor does not is certainty: the figure you were shown is the figure you pay, whatever the traffic does, whatever the route does, whatever the day does.

The price has to be decided by the party who is accountable for it. If the number can be set anywhere else, the business is not making a promise, it is accepting a suggestion.

And the promise has to survive the world moving. A flight lands ninety minutes late, a road closes, a job takes longer than anyone expected. The price was the promise, and re-quoting when circumstances change is the same as never having fixed it.

That is why this rebuild is not really a story about a vulnerability. It is a story about where a commercial commitment is allowed to be made.

The four decisions that mattered

If you read nothing else on this page, read this.

One
The server decides anything that costs money

A price, a discount, a class, a charge. The device asks. It never tells.

Two
A quote is an object with an identity, not a number that gets passed around

What was agreed and what is charged are the same thing rather than two values that have to match.

Three
An airport booking follows the flight, not the clock

Because the moment a pickup should happen is decided on the day, by somebody else, repeatedly.

Four
Some passengers cannot report a problem, so the platform has to notice

A missed school run is not a complaint waiting to arrive. It is silence.

Decision one

The server decides anything that costs money

Every application draws a line between what it computes and what it accepts from outside. Put that line in the wrong place and no amount of care above it helps, because the decisions being trusted were never the application's to begin with.

In the inherited build the fare was worked out on the customer's own device and submitted with the booking. The server was not calculating anything. It was writing down a number it had been handed and calling it a booking.

It was the one number that mattered. A tampered preference is a nuisance. A tampered price on a fixed-price business is the product failing at its only promise.

It produced no signal. A booking made at the wrong price is a valid booking with a valid customer taking a real journey. Nothing looked wrong on any screen anybody was watching.

What we did

Pricing moved to the server entirely. The fare is calculated where the business is accountable for it, and what the device sends is a request for a quote rather than an assertion of one.

What the customer sees is a display of a decision, not the decision itself.

And the rule generalises past price. Anything that costs money, grants access or creates an obligation is decided server-side. A discount, a vehicle class, a cancellation charge, a permission, an account balance. The device asks. It never tells.

Secrets moved to where secrets belong. Credentials were removed from client-delivered code and placed in server-side secret management. Removal is only half of that job, since anything that was exposed has to be treated as compromised and replaced rather than merely hidden.

Impact

The fixed price became fixed. Which is not a feature, it is the business finally doing the thing it advertises.

A class of failure was designed out rather than patched. The question stopped being whether a given field is validated and became where decisions are made at all.

The leak closed and stayed closed. A fix that depends on remembering to validate each new field reopens with the next feature. A fix that moves the decision does not.

A whole category of future work got cheaper. Every subsequent feature that touches money inherits the boundary rather than re-litigating it.

Decision two

A quote is an object, not a number

Moving the calculation to the server solves who decides. It does not by itself solve a subtler problem, which is that a price has to survive the gap between being quoted and being charged.

A customer is shown a fare. They think about it. They book, perhaps minutes later, perhaps the next day. Something has to connect the figure they were shown to the figure they are charged, and if that something is the number itself being carried back and forth, then the two are separate values that have to be compared and hopefully agree.

That is the same weakness in a different costume. It is also how a customer ends up being charged a fare that is technically correct and not the one they agreed to, which on a fixed-price business is the same failure from the other direction.

What we did

A quote is issued with its own identity and its own life. It is a thing the server made, with a price, a route, a vehicle class and a moment it stops being valid.

A booking refers to the quote rather than restating it. What was agreed and what is charged are one object, so there is nothing to compare.

Expiry is a state the system understands. A quote from last week is not a number somebody kept, it is a quote that has expired, and the system says so rather than honouring or silently re-pricing it.

Vehicle class is part of the quote. A price for a van and a price for a sedan are different promises about different things, and conflating them is how a customer arrives at a different car than the one they paid for.

Impact

There is no gap between the agreed price and the charged price, because they are the same record.

A stale quote behaves predictably. The customer is told, rather than being either overcharged or given a price the business no longer wants to honour.

Disputes have an answer. What was quoted, when, for what, and what was charged against it, are one traceable thing rather than a reconstruction.

Pricing can change without breaking outstanding quotes, because a quote already issued carries its own terms.

Decision three

An airport booking follows the flight

Airport transfers are the operator's core work and the hardest thing to schedule, for a reason that has nothing to do with software.

The pickup was agreed weeks ago. The moment it should actually happen is decided on the day, by an airline, and revised more than once.

If a booking is a fixed time, a ninety minute delay puts a driver at a terminal for an hour and a half with another job after this one. Somebody pays for that. The passenger, who did not cause it. The operator, who cannot bill for it. Or the next passenger, whose car is now late. And the fixed price makes it sharper rather than easier. The fare was the promise, so re-quoting because a plane was late is the business breaking its own commitment over something the customer had no part in.

What we did

An airport booking carries the flight it belongs to. When the flight moves, the pickup moves with it, and the dispatch board knows before the driver sets off.

The dispatch time is landing time plus how long an airport actually takes, which differs between a domestic arrival with hand luggage and a long-haul one with a family and a baggage hall.

The driver is told a revised time, not a cancellation and a rebooking, so the job stays with the person who planned their day around it wherever it still can.

There is a threshold at which it becomes a different job. A flight moved by six hours is not the same booking, and the platform needs a defined point rather than silently reassigning at any distance.

The fare does not change. The passenger did not cause the delay, and the price was the promise.

Impact

Drivers stop waiting at terminals for delays nobody told them about, which is the largest avoidable cost in airport work and the thing drivers resent most.

The passenger is met when they actually arrive, which is the whole reason somebody books a transfer rather than joining a rank.

A delayed flight stops being a dispatcher's midnight problem. The decision is made by the system on information the system already has.

The fixed price survives contact with the real world, which is the only test that matters for a business selling certainty.

Decision four

Some passengers cannot report a problem

The operator runs contracted school and medical transport alongside its commercial work. Those look like ordinary jobs on a dispatch board and are not.

The passenger did not book the journey. A school, a clinic or an authority arranged it. The person travelling has no account, no app, and frequently no practical way of raising a problem.

Which means nobody reports a no-show. On a commercial job the passenger telephones when the car does not arrive, and that call is the platform's error detection. Here there is no call. A missed school run is a child on a pavement and nobody has been told.

And who drives matters more. Contracted transport of children and patients usually carries requirements about consistency of driver and current clearance, both of which are conditions of doing the work rather than notes in a file.

What we did

Absence became an event. A pickup that has not happened by its window raises an alarm to a person within minutes, rather than surfacing when a driver files their sheet at the end of a run or in a report the next morning.

Assignment carries conditions. Where the contract requires a named or consistent driver, or current clearance, that is a condition of the job being assignable rather than something checked afterwards.

Arrival and departure are recorded, because a contracting authority reviewing performance asks about punctuality and driver continuity, and both have to be evidence rather than recollection.

And it runs on the same engine. An airport transfer, a business account and a contracted school run are one object with different conditions on assignment, different escalation thresholds and different rules about who may drive them.

Impact

The gap between a car being late and somebody knowing about it is measured in minutes. On this kind of work that gap is the entire risk.

A contract can be evidenced rather than defended. Punctuality and continuity are recorded, so a performance review is a report rather than an argument.

Safeguarding requirements are enforced at assignment, which is the only point at which enforcing them prevents anything.

One dispatch engine serves every segment, so an improvement to routing or tracking reaches all of them at once.

The rest of the operation

Real-time booking and dispatch across a mixed fleet. Standard cars, an electric option, comfort vehicles and vans seating up to eight, each a different vehicle class with its own fixed price, matched to the job rather than to whoever is nearest.

Live tracking and estimated arrival, so a passenger waiting at a door knows whether to put their coat on.

Payment in the way people actually pay. In the app, by card in the car, or in cash. Each one settles against the same server-owned fare: three payment methods, one price, and no route by which the amount charged can differ from the amount agreed.

Cash needs its own accounting. Money taken in a car is the operator's revenue sitting in a driver's pocket, and a platform that records the fare but not the settlement has a reconciliation problem it will discover at month end. A cash job is not complete when the passenger leaves. It is complete when the money arrives.

And a back office that runs all of it. Fleet, drivers, bookings, accounts and payments, with permissions that differ by role because a dispatcher, a driver and an owner should not see the same things.

The controls underneath

Nothing that matters is decided on a device. Which is the audit finding turned into an architectural rule, and it covers permissions and access as well as money.

Secrets live in server-side secret management and are not shipped to a browser or an app. Anything previously exposed is treated as compromised and replaced rather than merely removed from view, because removal without rotation is tidying rather than remediation.

Passenger data is minimised and scoped. A driver needs a pickup point, a destination and a name. They do not need a passenger's history, and the platform does not offer it.

Location data has a purpose and a life. Tracking a vehicle during a job is operationally necessary. Retaining a detailed movement history of every driver and passenger indefinitely is a different thing, and under European rules it needs a reason and a period rather than a default.

Three classes of event are recorded with actor and time. Who changed a fare or applied a discount. Who assigned or reassigned a job. And who accessed passenger records. These are the three that get questioned afterwards, by a customer, by a contracting authority, or by a regulator.

Payment settlement is reconciled rather than assumed, across all three methods, because a fare marked paid and a payment received are two different claims.

Which criteria this rebuild was really about

There is a neat symmetry to this project. The trust services criteria are the questions an auditor asks, and this engagement began with an audit. What the criteria would have asked is very close to what we did ask.

Processing integrity is the criterion the original build failed, and it failed completely. The criterion asks whether a system does what it claims, completely and accurately. A platform that accepts a commercially significant number from an untrusted party, acts on it, charges against it, and reports nothing has no answer to that standard at all. Server-owned pricing, quote identity, expiry as a state, and settlement reconciled across payment methods are all answers to that question rather than to a security one.

Confidentiality is the second, and it is where the secrets finding sits. Credentials delivered to a client device are, by construction, not confidential, and the remedy is rotation rather than concealment.

Privacy is the third and it is quieter. This platform knows where people went, when, and with whom, including children and medical patients. Under European rules that requires a purpose and a retention period rather than an assumption, and the operational value of tracking a vehicle during a job does not extend to keeping a movement history of everybody forever.

The control set was designed against the criteria rather than mapped to them afterwards. That is a statement about how the platform was rebuilt. It is not a claim to hold an attestation, and where the operator needs one, the work is theirs to complete with an auditor.

The constraints we designed around

Taking over somebody else's build introduces constraints that a fresh start does not.

The business could not stop

The challenge. This is a twenty-four hour operation with real passengers and real contracts. A rewrite is not a pause, and nobody was going to accept one.

What we did. Rebuilt around a running business, with the old system carrying work until the new one could take it, and cut over by area rather than in a single event.

Impact

The rewrite happened without the operation stopping, which is the only version of a rewrite a working company can accept, and the reason most necessary rewrites never happen.

A rewrite has to be argued, not announced

The challenge. Telling a client the build they already paid for cannot be safely extended is telling them money is gone. It is the most expensive recommendation available and the one they least want to hear.

What we did. Evidenced it. The audit produced findings a client could see for themselves rather than a conclusion they were asked to accept, and a recommendation with alternatives rather than a single option.

Impact

The decision was the client's, made on evidence. An audit that always concludes rewrite is a sales process wearing a technical costume, and a client who suspects that is what happened will not act on it.

Mobile and web have to agree

The challenge. Two front ends, both able to book, both showing prices, both talking to the same operation. Two implementations of a pricing rule is two chances to get it wrong, and the inherited build had already demonstrated where that leads.

What we did. Put every commercially significant decision behind one backend, so both clients ask the same question of the same authority and neither can answer it themselves.

Impact

The web and the app cannot disagree about a price, because neither of them knows one. And a pricing change ships once.

Drivers are not office staff

The challenge. A driver uses this at a kerbside, in traffic, at night, often between two jobs. Anything requiring attention takes it from somewhere it was needed more.

What we did. Kept the common path short: accept, navigate, arrive, complete. Everything else is available and out of the way.

Impact

The app gets used as intended rather than worked around, which for a driver-facing tool is the only measure that matters.

The impact, depending on your job

If you run the business

An audit is cheap relative to what it finds. This one located a leak in the company's single commercial promise before it was built on further, and the cost of finding it was a fraction of the cost of another year of not knowing.

A rewrite recommendation you can see the evidence for is worth more than a reassurance. Any supplier will tell you the previous one did it badly. Fewer will show you.

The promise became real. A fixed price that can be edited is marketing. A fixed price the server owns is a product, and it is the only thing separating this operator from a metered competitor.

If you run engineering

The trust boundary is the part worth your attention, because the finding generalises well past this project. The question is not whether a given field is validated. It is which decisions the system makes and which it accepts, and any commercially significant decision on the accepting side is a defect regardless of how carefully the field is checked.

Then quote identity, which closes the same gap in time rather than in space.

If you run a delivery team

Most of what is on this page came from deciding where things live. One backend owning every commercial decision, so two front ends cannot disagree. One dispatch engine with per-job conditions, rather than a product per segment. One quote object rather than a number in transit.

And the audit itself, which is a delivery practice rather than a security one. Knowing what you have been handed is the first task on any inherited codebase, and skipping it is how a team commits to somebody else's decisions without ever seeing them.

If you own the numbers

The revenue leak closed on the day the fix shipped, and it was recurring rather than one-off.

Driver time at airports stopped being spent on other people's delays, which is paid time producing nothing.

Cash reconciles, so revenue taken in a car reaches the business rather than becoming a month-end discrepancy.

One backend serves both platforms, so a pricing or commercial change is one piece of work rather than two and a divergence.

Talk to the client, not just to us

Everything on this page is our account of our own work. If you are seriously evaluating us, we will arrange a reference call with a client who has been through a build like this one, and you can ask them the questions you would rather not ask us.

Request a client reference
Built with

Technologies we built with

We name the layers rather than the suppliers on the passenger data path, because a supplier map is our client's exposure rather than our credential. Full detail available under NDA.

Backend
JavaSpring BootPricing, quoting and settlement resolved server-side
Frontend
Next.jsReact
Mobile
React NativeiOS and Android
Data
MySQL
Cloud
AWSServer-side secret managementNo credentials in client code
Integrations
Payment providers (in-app and in-car)Mapping and estimated arrivalFlight status for airport bookings

More case studies

View all case studies

Inherited a build, and unsure what is in it?

Tell us what you're working with, and we'll tell you honestly what we'd check first.

Book a discovery call