A travel agency: its own catalogue, booking in three steps
Client product · three languages, CMS, seat inventory, booking, deploy and handover
VeTravel sells package tours, excursions and coach hire from Bosnia and Herzegovina. The new site runs in Bosnian, English and German: a catalogue with filters and a search that takes a whole sentence, tour pages with a day-by-day programme and a table of departures with live seat counts, booking in three steps, and request pages for coach hire, flight tickets and trips built to order. The agency runs all of it from the admin panel, in its own language.
How it worked before
The old site was a shop window: the catalogue was empty, and the only way to buy a tour was to write through a form, a Viber community or a WhatsApp group.
What made it hard
A coach has a fixed number of seats, and two people can reach for the last one in the same second. The owner writes in Bosnian, has no developer, and will not type every tour three times. Browsers ship incomplete Bosnian locale data, so a date or a price can render one way on the server and another on the phone. And card payment is not a technical question: the usual gateway does not serve Bosnia, so the booking path has to work without a card.
Engineering decisions
- 01
The database refuses to oversell
Seats are claimed by one conditional UPDATE: of two requests for the last seat, the second matches zero rows and gets a plain “sold out”. Underneath sits a CHECK constraint, seats taken ≤ capacity, so even code that bypasses this path — or a query typed by hand — is refused.
- 02
A held seat comes back without waiting for a scheduler
Step one holds the seats for 30 minutes before any details are typed. The hosting plan allows a cron once a day, which would quietly turn 30 minutes into 24 hours, so every new booking first reclaims the lapsed holds on its own date. The cron is only a backstop.
- 03
Bosnian is the source, English and German fill themselves
Validation applies only to the source language, so a tour can be published in Bosnian alone. On publish, empty languages are translated by the model and marked “needs review”; one hand-written paragraph and that language is never touched again. A reply that loses a paragraph or a line of the “included” list is rejected whole.
- 04
A sentence becomes filters, deterministic first
“more u avgustu do 1000 KM autobusom” fills four filters and shows what was understood. A parser handles month stems across Bosnian declension, price ceilings, transport and destinations in three languages with no network call. The model sees only what is left, answers through a four-field tool, and its output is validated back into the shapes the URL accepts.
- 05
Limits on what a stranger can make the site do
Admin passwords of at least 12 characters (the package default was three), a lockout after five failed attempts, rate limits on seat holds, forms and paid model calls. Counters live in Postgres, because memory on serverless limits nothing, and an IP is kept only as a salted hash that is gone within the hour.
What else is in there
A comparison of two or three tours that shows only what differs, a printable trip plan, a price for the whole family or group, a coach-hire estimate where the money is computed in code and the model supplies only the road distance, a destination map with no tile provider, reviews and video reviews that cannot be published without a consent tick, a passenger manifest as CSV, gap-free document numbers from a Postgres sequence, and a card-payment branch kept dark behind a flag, its idempotent callback proven by tests.
Bugs only production showed
Chrome claims to support Bosnian and prints a date as “2026 M10 17” — replaced by an explicit formatter for all three languages. Photographs went out through a serverless function, and a 146 KB image took 10–22 seconds; they now come straight from the CDN. Untranslated tours led to a 404 in English and German until one slug resolver took over. After the move to the custom domain the admin login failed silently between www and the apex; one host now serves the app, enforced in code.
Where it stands
Live on the agency's own domain in three languages, with a written handover of what is done and what waits on the owner. Card payment is built behind a flag and waits on a gateway the agency can contract; the model features switch on with one environment key, and everything works without it.
- A multi-tenant restaurant platform: app, back office and three workers45,000
- A travel agency: its own catalogue, booking in three steps27,000
- Water delivery, end to end22,100
- A clinic: 52 pages, an assistant and a smile simulator14,500
- An outreach automation engine11,000
- A relationship CRM with an AI assistant10,400
- Direct-message sales run by an AI4,800
- Daily quests with an AI game master3,700
- A catalogue that updates itself3,300
- A platform for authored training programmes3,300
- A funnel that qualifies the lead itself2,900