Project facts
- Client
- Tour operator in Chiapas (anonymous)
- Timeline
- July – August 2026
- Status
- v1.2.0 in production
- Role
- Product, architecture, build and deployment
- Replaces
- A hand-made quoting flow built on spreadsheets and text documents
Context
A tour operator in Chiapas sold packages by quoting in spreadsheets and pasting the result into a branded text document. Every customer change — one more room, one less transfer — meant redoing both by hand.
The goal was twofold: take repetitive work away from the salesperson, and give the customer something better than an email attachment. That is, a link where they can move the variables that actually matter to them and see the effect on their total, without having to write «what if there are four of us instead of two?».
With one underlying condition: in this business, margin is confidential. The customer must be able to play with their package without ever getting close to what that package costs the agency.
The problem
A public link that computes prices is, by definition, a link that can leak how they are computed.
As soon as the customer can recompute their total in the browser, the temptation appears to send them the data and let the browser do the maths. And that is where the business leaks: per-item costs, margin tiers, supplier names. It does not need to be shown on screen; it is enough for it to travel in the response.
I saw the other path up close, and its bill. In a sibling project the pricing engine was duplicated in the browser and on the server, and that duplication is today its worst debt: the two engines never match exactly, and every divergence is an argument with a customer who saw a different number from the one in the final document.
There was also the problem of time. A quote that has been sent and not answered is potential revenue going cold, and the salesperson has no way of knowing which ones are worth chasing.
Key decisions
A guard that inspects every public response
Everything leaving through the public route passes through a function that inspects the payload and throws if it finds cost, margin or supplier data. It is not a team convention: it is a check that runs in production, on every request.
Rules that depend on everyone remembering break the day someone adds a field in a hurry. A runtime guard turns «we must not leak» into «we cannot leak», and it fails loudly in development before it can fail quietly in production.
Price is computed on the server only
The customer's browser shows the total the server gives it; it has neither the pricing engine nor the data to reproduce it.
Duplicating the engine duplicates the truth. I watched it become the worst debt in a sibling project: two implementations that never agree to the last decimal, and a recurring argument about which one is right.
Freeze the cost and sign the selection
Every item stores a snapshot of its cost at quoting time, the reference number is atomic and never recycled, and the customer's selection carries a SHA-256 fingerprint that invalidates it if the salesperson edits the quote afterwards.
Without a snapshot, last week's quote silently changes when a rate changes. Without a fingerprint, the customer can accept a version that no longer exists, and the business ends up bound to something it never offered.
Choose the stack for what it consumes, not for what is fashionable
A lightweight data-access layer was used instead of the usual option, and an in-process PDF generator instead of spinning up a full browser to render. The difference is on the order of 250 MB of memory.
All of this lives on a 16 GB server already running more than 60 containers. There, 250 MB is not premature optimisation: it is the margin between coexisting with the other services and starting to compete with them.
The screen that pays for the project
One view lists the quotes the customer opened and has not accepted, ordered by a deterministic score whose rules the interface spells out in words: «Opened 3 times · Read for 2 min · Expires in 2 days».
Zero artificial intelligence, and it works from day one, because the rules are visible and the salesperson can agree or disagree with them. A score nobody understands does not get used; one that explains itself does.
From the internal quote to the customer's link
Price is born and dies on the server. The customer's browser paints a total, never computes one, and the response passes a guard before it leaves.
The salesperson builds the quote
Picks services and items; costs are frozen in a per-item snapshot.
Atomic reference, never recycled
Each quote gets a unique reference number that is never reused.
Tokenised link
The customer opens their link and picks occupancy and add-ons, with a live total.
assertNoLeak()
Every public response is inspected: cost, margin or supplier data throws an exception.
Branded PDF on demand
The document is generated on the spot, with the selection signed by its fingerprint.
The solution
It is a monorepo with a Fastify backend over PostgreSQL and a React interface in TypeScript. The salesperson works in an installable app that assembles the quote; the customer receives a tokenised link that opens their own view, where they pick occupancy and add-ons and watch the total update.
When the customer confirms, the branded PDF is generated on the spot, with the selection already signed. The document is not stored in advance nor regenerated differently: it comes from the same source as the total the customer saw on screen.
And on the internal side there is the follow-up screen, which turns «I have a lot of quotes out there» into a list ordered by likelihood of closing, with the reason written next to each one.
Results
- v1.2.0 in production, replacing the spreadsheet-and-document quoting flow.
- Every public response passes the anti-leak guard: cost, margin and supplier cannot leave through that door.
- Price has a single implementation, on the server: there are no two engines that can disagree.
- Atomic, never-recycled reference numbers, per-item frozen costs, and a SHA-256 fingerprint that invalidates a stale selection.
- The stack choice saves on the order of 250 MB of memory on a server shared with more than 60 containers.
What I learned
That confidentiality is either designed in or it does not exist. Writing «do not expose margins» in a style guide prevents nothing; a check that blows up the request does.
That seeing another project's debt up close is worth more than reading about it. The duplicated pricing engine was not avoided out of doctrine, but because I had already seen the monthly bill it charges.
And that the feature which pays for the project is almost never the most sophisticated one. Here it was an ordered list with rules explained in plain language, without a single line of artificial intelligence.