Skip to content

Eman Way · Oct — Dec 2025

A travel and Umrah platform that stays correct across roles, regions and payment rails

A ten-module booking platform for Umrah and travel, with three user roles, region-scoped access, Stripe and FPX payments, and the accounting behind them.

The Eman Way marketing site, showing a package search form over a photograph of the Malaysian highlands.

Context

Umrah and pilgrimage travel is sold through agents. A customer books through someone they know, that agent works for or with an agency, the agency owes commission to the agent, and everybody involved needs a different view of the same booking. Most of the operators in this market run on a mix of spreadsheets, WhatsApp and a payment link, which works until two people edit the same booking or the commission ledger stops matching the bank statement.

Eman Way is the platform that replaces that. I led the project and built it across October to December 2025 — the Flutter client, the PostgreSQL schema, the API, and the deployment on Google Cloud.

Problem

The hard part of this system is not the booking form. It is that the same booking means different things to three different people, and the platform has to serve all three without leaking anything between them.

A customer sees their own trip and what they still owe. An agent sees their own customers and their own commission, and must not see another agent's book of business. An admin sees everything, but "everything" is scoped by region — an operator running Malaysia should not be reading the Indonesian ledger by changing an ID in a URL.

On top of that, money arrives by two different rails. International cards go through Stripe. Malaysian customers overwhelmingly pay by FPX bank transfer, which settles differently and fails differently. Both have to land in one consistent record of what has been paid.

Approach

Roles and regions as one access rule, not two. Every query is scoped by the acting user's role and their region in the same place, at the data-access layer, rather than being enforced per-screen in the client. There is one function that answers "what may this user see", and every read goes through it. This is less flexible than per-endpoint rules and slightly more awkward when a screen genuinely needs a cross-region view, but it means access control cannot be forgotten in a new feature — the only way to fetch data is through the scoped path.

The booking lifecycle as an explicit state machine. A booking moves through enquiry, held, part-paid, confirmed, travelled and closed. Each transition has a condition and a record of who caused it. Progress tracking on the customer's screen is a projection of that state, not a separate field somebody has to remember to update. When the two are separate, they drift.

Two payment rails, one ledger. Stripe and FPX both write into a single payments table with the rail recorded alongside the amount, and the booking's paid total is derived from that table rather than stored on the booking. FPX's asynchronous confirmation means a payment can be pending for a while, so the state machine distinguishes "initiated" from "settled" and the UI never tells a customer their trip is confirmed on the strength of a redirect.

A schema that can answer the accounting questions. Commission, reporting and reconciliation were designed into the tables at the start, not bolted on. Commission is computed from the booking and the agent's rate at the time of sale, and stored as a row rather than recalculated later, so a rate change next quarter does not silently rewrite last quarter's ledger.

Deployment. The API runs on Google Cloud behind a caching layer for the read-heavy catalogue endpoints — packages, availability, itineraries — which are hit constantly and change slowly. Authentication is Google OAuth, which removed a password reset flow nobody wanted to own.

Decisions and trade-offs

Derived totals over stored totals. Computing what a booking has been paid from the payments table is slower than reading a column, and it needs the caching layer to stay comfortable. But a stored total is a number that can disagree with reality, and in a system handling other people's pilgrimage payments, a total that can be wrong is not an acceptable trade for a faster query.

One client for three roles. Customer, agent and admin all use the same Flutter application with different capabilities, rather than three separate builds. That keeps one codebase and one release, at the cost of shipping code to customers that only admins can invoke. Since authorisation is enforced server-side, that is a size cost rather than a security one.

Scoping first, features second. Region-based access went in before the feature modules did, which pushed the first demo later than the client wanted. It also meant the tenth module inherited the access rules for free instead of needing its own audit.

Result

Ten-plus modules covering the booking lifecycle end to end: enquiry, holds, payments on two rails, automated progress tracking, commissions, reporting and accounting. Three roles with region-scoped access enforced at the data layer. Deployed on Google Cloud with secure REST APIs and a caching layer in front of the read-heavy paths.

  • The Eman Way mobile app home screen on a phone, with a package search field, an Umrah packages banner and a row of Malaysian destinations.
    The app opens on search, with Umrah and leisure packages side by side.
  • A package detail screen in the Eman Way app, showing a five-day Malaysia cultural escape with a rating, an itinerary tab and a per-person price.
    A package, its itinerary and its price, on the phone.
The Eman Way marketing site, showing the package search form over a photograph of the Malaysian highlands, with curated destination cards beneath it.
The public site. Search by dates and travellers, then browse curated packages.
The Eman Way admin payments screen, showing collected, pending, failed and refunded totals above a transaction table listing payments taken through Stripe, FPX, bank transfer and manual entry.
Both payment rails land in one ledger, with refunds and receipts on each row.
The Eman Way agent dashboard, showing total bookings, travellers and revenue, a shareable agent booking link, and a table of recent bookings with their status.
An agent sees only their own bookings, and gets a referral link that attributes them.
A traveller record in the Eman Way admin, showing passport and nationality details alongside visa status, reference number and approval dates.
Traveller and visa state, per booking, with the document trail attached.
The customer account area of Eman Way, listing booked packages with the amount paid and a payment status of paid, failed or under review.
What the customer sees — their bookings, and exactly where each payment stands.
Diagram of the Eman Way booking lifecycle, showing a booking moving from enquiry through payment via Stripe or FPX to confirmation, with the customer, agent and admin roles that can act at each step.
The state machine behind all of it, and which role may move a booking at each step.

Want the detail behind any of this? Email me.