By Muhammad Faisal · Week ending October 8, 2026
This week I shipped five phases of a customer mobile app, added permanent customer records to a POS system, and chased an email bug that returned success without sending anything. The three jobs looked unrelated, but one principle tied them together: the client should validate for usability, and the server should decide what is true.
1. A design system before features
The mobile app started with a mismatched green and beige palette and a horizontally scrolling bottom bar. Before adding any booking screens, I fixed the foundation.
- Semantic tokens. A single tokens file defines slate neutrals, sky for read actions, emerald for create, indigo for save, red for danger, teal for export and purple for special actions. Screens ask for a tone such as “create”, never a hex value, so a palette change touches one file.
- A 4px spacing grid with shared typography, radius, shadow, input and button styles.
- Fixed bottom navigation. I replaced the scroll view with a flex row where every tab gets equal width, an icon, a label and an active state.
- Real safe areas. The app is wrapped in a safe-area provider so headers and tabs clear notches and home indicators. I installed Expo’s compatible module instead of guessing padding.
The same discipline applied to the API layer. One module owns route paths, the HTTP client owns host composition, and feature modules own typing. When the client had a silent localhost fallback, a missing configuration looked like a server “endpoint not found”. Now a missing base URL fails with a specific configuration message, and the production HTTPS base is set in one place.
2. Booking flows that respect the backend
I audited the web frontend and backend source before typing any contract, then built three flows in order.
Seated experiences (cinema, events, parties, seminars). The seat map is derived from capacity, booked seats and permanently reserved seats in the offering response. The submit payload is deliberately small: an offering ID and a list of attendees with name, gender, seat and a self flag. The backend overwrites the self attendee’s name from the authenticated account and returns HTTP 409 if inventory changed meanwhile. The UI shows that message directly instead of hiding it.
Travel. The itinerary API supports connections and one master booking, so I used it instead of the legacy single-route endpoint. The tricky rule is that every leg must carry the same passengers in the same order, yet each leg is a different bus with different seats. I modelled each passenger as stable identity plus a list of seats aligned to the journey’s legs, and let the API adapter turn each entry into that leg’s seat code. That keeps editing simple while preserving the server’s invariant.
Dinner. This is not numbered seating. It is overlapping capacity per area, with restaurant-local opening hours and billing at the table. Keeping it separate from seat checkout avoided forcing one contract onto a different problem.
Then came tickets, cancellation and reschedule requests, and live tracking. Requests stay pending review, because the backend owns eligibility, price settlement and availability at approval time. Tracking polls every five seconds for broadcasts the backend has already filtered to the customer’s confirmed trips. With no map SDK configured, the screen shows coordinates, freshness, delay and upcoming stops, plus a handoff to the device’s Maps app, rather than embedding a map that does not work.
3. Permanent customers on POS bills
IQ-POS bills now require a customer name and an international phone number. Phones are normalized to E.164 syntax: a plus sign followed by 8 to 15 digits, with spaces, brackets, dots and hyphens stripped before lookup.
The design choices matter more than the regex:
- Uniqueness is per retailer and phone, matching the existing table constraint.
- The unique constraint arbitrates concurrent requests. The resolver inserts with a conflict clause on that key, so two simultaneous bills cannot create duplicates.
- Names are compared, never overwritten. The same phone with a different name returns HTTP 409.
- Resolution and bill creation share one transaction, so a conflict cannot leave a half-written sale.
- Each bill stores a snapshot of name and phone, so receipts do not depend on a mutable profile.
The old customer reference pointed at user accounts, not the customer table. Changing that foreign key could have broken history, so I added a new column beside it. Old completed bills have no recoverable phone, and the migration does not invent one.
4. The bug that returned 200 and sent nothing
Users saw a success message from resend-verification and never received an email. The cause was a single boolean. The OTP service returned false for four different situations: unknown account, already-verified email, suspended account, and a disabled permission. The controllers ignored it and always answered 200.
For password recovery that is correct, because a public endpoint must not reveal which emails exist. For resend-verification it is misleading, because the user is authenticated and deserves to know the email was already verified.
The fix I proposed is a typed result with statuses such as sent, already verified, not allowed and account not found, with each endpoint choosing its own public response. I also separated three stages that are easy to blur: the app deciding to skip, the SMTP server rejecting the message, and the mailbox provider failing to deliver after accepting it.
A follow-up error, OpenSSL’s “wrong version number”, pointed at a TLS mismatch. Port 587 expects a plain connection upgraded with STARTTLS, so it pairs with the secure flag off. Port 465 uses TLS from the first byte, so it pairs with the flag on. Setting 587 with the flag on fails the handshake before any mail is attempted.
What I took away
- Audit contracts from source. Reading the backend and web code before typing a payload turned contract drift into compile errors instead of production bugs.
- Centralize vocabulary. Tokens for visuals and an endpoint registry for routes mean one edit changes everything.
- A boolean is a lossy signal. If callers need to treat outcomes differently, return a type that says which outcome occurred.
- Let the database arbitrate races. A unique constraint inside a transaction is stronger than any check in application code.
- Accepted is not delivered. SMTP acceptance, inbox placement and user-visible success are different events and should be recorded separately.
Next week: implement the typed OTP result with its six tests, apply the POS migration to the target database, and wire forgot-password once the backend exposes request and reset endpoints.