Case study · Marine & Leisure
A fishing charter, run end to end — for World Marine Center
Six boats, three-hour trips, and a legal obligation to identify every guest who steps aboard. We built the customer app, the staff panel and the booking site that run it — with the guests’ identity documents held in Doha and no way for two parties to be sold the same hull.

At a glance.
- Client
- World Marine Center, Qatar
- Sector
- Marine & leisure — fishing charters
- Fleet
- 6 boats · 3-hour trips · up to 6 guests
- Built
- Customer mobile app, staff admin panel, public booking site
- Stack
- Next.js & Postgres on Cloud Run · Flutter for iOS and Android
- Hosting
- Google Cloud me-central2 (Doha)
- Verified
- 98 backend tests · 72 app tests · 35 manual checks
- Status
- Built and tested, ready for deployment
What a charter operator is actually juggling.
A fishing charter looks simple from the outside: pick a boat, pick a time, go. Running one is not. Six boats, nine start times a day, three-hour trips, and every seat carrying a legal obligation that has nothing to do with fishing.
The same boat, promised twice
A three-hour trip does not just occupy its start time. A boat leaving at 09:00 is unavailable at 10:00 and 11:00 too. Track that across six boats in a chat thread and a double booking is a matter of when, not if — and it is discovered at the marina, in front of the guests.
Every guest needs papers
Passengers leaving a Qatari marina must be identified. Not the person who booked — every guest aboard. A booking taken over the phone with “five people, names to follow” is not a booking, it is a problem for the morning of the trip.
Documents you cannot mishandle
Those papers are Qatari IDs, visas and passports. Collecting them creates a duty: to keep them in the right place, show them only to the right people, and be able to say afterwards who looked at what.
No single view
Bookings in WhatsApp, availability in someone’s head, maintenance on paper, crew certificates in a folder. Every question — can we take a party of six on Friday? — needs a person to answer it.
One system, three ways in.
We built a single platform with three faces onto it, so the fleet has one source of truth no matter who is asking.
A customer app
iOS and Android, built in Flutter. Browse the fleet, see live availability and real pricing, upload each guest’s documents, and track a booking by its reference. Deep links open a specific boat, package or trip straight from a message.
A staff admin panel
Eight screens covering trips, boats, captains, customers, enquiries and staff accounts. Confirm or refuse requests, assign boats, and see the day at a glance.
A public booking site
For people who arrive from a search rather than the app. It takes an enquiry — date, hour, guest count — and deliberately cannot create a trip, because a website form is the wrong place to collect a passport.
A documented API
Everything runs through one versioned API, so the app, the panel and the site can never disagree about whether a boat is free.
The rule the whole system exists to protect.
Nothing a customer can do produces a confirmed booking.
A request arrives with a reference and the status requested, and no boat assigned. A person at World Marine confirms it and chooses the boat. Until then the seat is not sold and the vessel is not committed.
This sounds like a limitation. It is the opposite: it is the only way an operator can trust the screen. Software that lets a customer mark themselves confirmed will eventually confirm two parties onto one hull, and the marina finds out before the office does.
The refusals are just as deliberate. Book without guest details and the request is refused. Book for five guests and supply one set of documents and it is refused with the reason spelled out — “Identity details are required for all 5 guests — 1 supplied.” A refused booking leaves nothing behind: no half-written trip, no phantom hold on a boat.
No boat is ever promised twice.
Availability is computed from the three hours a trip actually occupies, not from its start time.
- An overlapping trip on the same boat is refused — “Fishing Boat 01 is already booked for part of that time.”
- Back-to-back is allowed. A 09:00 trip frees the boat at 12:00, so a 12:00 departure is fine.
- Cancelling releases the hold immediately, and the slot can be rebooked.
- Booking all six boats at 09:00 removes 08:00 from the day as well — an 08:00 trip would still need a hull at 09:00. The subtle case is the one that matters.
Identity documents, handled like identity documents.
Collecting a passport is easy. Being trusted with it is the engineering.
Not everyone may look
Staff can run trips all day without ever seeing an ID number — the list shows it masked, and the download link is not rendered for them. Reaching a document by typing its address returns 403, not a login prompt.
Every read is on the record
Opening a document writes an audit entry. A refused attempt does not: the log records what was seen, not what was tried, so it stays a truthful account of exposure.
Uploads are checked properly
Files are validated by content, not by the name. A text file renamed .jpg is rejected. Anything over 8 MB is rejected with a plain message.
Customers are sealed off from each other
One customer’s trips are invisible to another. Cancelling someone else’s booking by reference returns 404 rather than 403 — refusing without confirming that the reference exists.
The data stays in Qatar — on purpose.
The platform runs in Google Cloud’s me-central2 region, in Doha. That is a decision, not a default.
The documents this system holds are Qatari IDs, visas and passports. Hosting them abroad would make every booking a cross-border transfer of personal data under Law No. (13) of 2016. Keeping the database, the container and the file storage in-country removes that question entirely.
The same reasoning drove the backup policy. World Marine retains identity documents indefinitely, so the database is the index to files that cannot be regenerated — which is why point-in-time recovery is switched on rather than nightly snapshots alone.
Tested against the running system, not the code.
Every claim on this page was checked by driving the software and reading the result back from the screen or the database.
98
backend tests passing, with TypeScript and ESLint clean
72
Flutter tests passing across the mobile app
35
manual checks against a live instance — 0 genuine failures
0
double bookings possible, proven by deliberately creating a collision rather than hoping to find one
The double-booking guarantee is a good example of the standard. The seeded trips happened not to overlap, so the first attempt proved nothing. We created the collision on purpose, confirmed the refusal, then checked that the refused trip had not been half-written.
Where it stands.
The platform is built, tested and documented to a deployment handover. It is not yet live, and these are the pieces that need a production environment rather than more engineering:
- SMS delivery needs a provider connected. The one-time-code flow and its rejection path are both verified against the development path.
- Push notifications need a Firebase project on the app side. The server side is covered by automated tests.
- Universal Links need a live domain and a signed release build. The association route correctly returns 503 until it is configured — it does not pretend to work.
Custom-scheme deep links already work: worldmarine:///fleet, worldmarine:///package and worldmarine:///trips/WM-2418 each open the right screen on a fresh install.
Running a fleet on a chat thread?
If your business is booked by message and tracked on paper, the failure mode is always the same — two customers, one asset, discovered too late. We build the system that makes it impossible.