Client engagement · Backend Engineer · Mar 2025 — Aug 2025
Sadaf
B2B Visa Capacity Platform
Delivered
What mattered
Per-viewer agency visibility is derived from routing path position, not stored as a column.
- The sending agency is derived from the viewer's index in the routing path. Never stored, so there is no filter to forget.
- Two separate gates: the status decides which agency may act, the role decides what that user may do.
- Entitlement is three stacked layers: plan tier, then feature rows carrying a min tier, then page permission.
- Passwords use bcrypt_sha256, so a long input is not silently truncated at 72 bytes.
System map
Context
Travel agencies resell visa processing capacity to each other, four tiers deep; an application travels up that chain and is handed to a third party at the top. 40 agencies, ~100 agents, 500–600 applications a day.
Three of us worked on it over five months, with the first two spent planning. Hooman built CoreApp, where the business rules live. I built PanelApp, the agency-facing service and UI, against the contract we designed together.
Two rules shaped my side of the build: an agency can only see its immediate neighbours in the chain, and each submit action has to move money and status together.
Privacy as a query, not a column
Every agency in a chain can see the application. None may see past its immediate neighbours: a tier-3 agency must not learn what the top charges, and the top must not learn who originated it.
The obvious implementation — a sponsor_id column, plus filtering on read — is
wrong twice over. It denormalises a fact that is different for every viewer, and
it means one missed filter leaks pricing between competitors.
So it is never stored. Each application carries a routing path, and the answer is derived from where the viewer sits in it:
who sent it to me = routing_path[N - 1]
who I sent it to = routing_path[N + 1] (N = viewer's index)
One row, and it renders a different, limited view to each agency that touches it. There is no denormalised field to fall out of sync, and no read path that can forget the filter, because there is nothing to filter.
Two separate gates on every action
Authority moves along the chain with the application, and that is separate from what a user's role permits. Both gates have to pass:
| Status | Which agency may act |
|---|---|
| TYPED | only the creator |
| SUBMITTED_L2 / L1 / L0 | only the agency currently holding it |
| READY_TO_SUBMIT | nobody — an automated process owns it |
Inside that, roles still apply: an AGENT can submit but cannot approve or
revoke. Keeping the two checks separate meant neither could be quietly widened
by a change to the other.
Entitlement and permissions, three layers deep
Not a role list — three stacked systems:
- Module —
AgencySubscriptiongates which modules an agency can open at all, by plan tier (STARTER < STANDARD < PREMIUM < ENTERPRISE, each inheriting the one below). - Feature —
ModuleFeaturerows carry amin_plan_tier, so individual features are gated by data rather than code, on a{module}.{section}.{action}convention:visa.application.create,accounting.analytics.anomaly. Selling a new feature tier is a row, not a deploy. - Page —
User → Role → Page → PagePermission(create/read/update/delete/approve/export), scoped by department.
Money and status move together or not at all
When an agency submits on behalf of one beneath it, the intermediary is both buying and selling. Two credit accounts are debited — the lower agency's with the middle one, the middle one's with the tier above — plus two status transitions, all in one commit.
There are five distinct routing scenarios with different money paths. A top-tier agency applying for itself has no credit account with itself, so it posts a direct bank expense instead.
The seam between the two services
PanelApp holds no business logic of its own; it drives CoreApp over HTTP. That
integration is a single client that owns retries, timeouts and the mapping of
CoreApp's error codes into something the panel can render, rather than
requests calls scattered through the views.
Authentication is two-layered: the panel proves it is the panel with rotated service credentials, then the user proves who they are.
I wrote a complete fake of CoreApp before CoreApp existed: same endpoints, same response shapes, realistic data, so the front end and client demos never blocked on the backend. Switching to the real service was a base-URL change. I deleted it when the real one landed, which is how I know the contract held.
Also mine on this service: the security layer (CSRF, rate limiting with automatic banning, session handling, audit logging), and a dropdown page that was issuing one request per option, which I cut to two requests total and from several seconds to under one.
The visa form
Fields, validation and family-group size limits vary by visa type and nationality, so the form is driven by that pair rather than by one static schema. Every name is captured in English and Arabic, with transliteration between them.
Result
The client ran it inside their real operation for a week. Feedback from that week led to substantial changes in accounting and the visa form. It reached a finished internal state, then paused with the client's business for non-technical reasons.
Screens


