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.

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 build

The app opens on search, with Umrah and leisure packages side by side. 
A package, its itinerary and its price, on the phone.






Want the detail behind any of this? Email me.