Problem
In Cuba, buying fuel or cooking gas means physical queues at the service centers, uncertain stock, and prices that shift across formats and octane grades. Avangenio set out to put that whole transaction ahead of the queue: let a customer see which nearby service centers actually have stock and at what price, reserve exactly what they need, pay, and show up to collect against a verifiable code — no guessing, no standing in line to find out there’s nothing left.
The hard part isn’t the storefront. It’s that an order can’t just be an internal record: it has to be registered against the external fuel system the service centers run on, which issues the authorization that makes a pickup real. So the product is a consumer marketplace bolted onto a third-party API contract, with identity rules (national ID or passport) that have to match that system exactly, and a physical hand-off at the end that needs to be tamper-resistant.
The platform is called Muévete y Cocina in-house — the name the codebase, the modules and the courier app still carry. What reaches customers are its brands: Mercaful, selling fuel and cooking gas, and Gasolero, fuel only. The screens shown here are Mercaful’s, and predate the second brand.
Approach
This was a team product, owned by one of Avangenio’s co-founders as product owner and directed by the company’s lead frontend engineer and a product designer. I worked within that structure, and what I contributed varied a great deal depending on the surface — from executing someone else’s specification to owning a component from the database table up.
-
The courier app — my initiative, my implementation. The team needed the web’s QR-scanner module to work offline-first, which the browser handled poorly. I proposed building it instead as a dedicated Flutter Android app, took an existing scanner flow as a reference, and rebuilt it from scratch in the new stack on my own terms, design included — delivered as MyC Dispatcher: the app a service-center courier uses to scan the customer’s QR, verify the order, and mark it delivered. I took it from the first commit through release hardening — camera lifecycle, offline-safe rejection of duplicate scans, ProGuard/R8 so the scanner survives a release build, dev/prod flavors, and a 90-day purge of old scans.
-
The customer web app — substantial contribution under direction. On the React Router 7 + Vite app I was one of the engineers executing the lead’s and the designer’s specifications, and I delivered a large share of it: the marketplace and live service-center map, the order wizard (grade, format, litres, pickup), payment and order tracking down to the empty / error / expired / rescheduled states, and the in-app wallet. I also connected the web app to the backend team’s fuel-order integration, working from the handoff spec they provided — the path that registers an order against the external fuel system and turns the returned authorization into the signed QR the customer presents at pickup.
-
Access control, down to the station. A courier should only be able to confirm deliveries at the service center they answer for. I designed and built that boundary through every layer: the dispatcher-to-servicentro assignment table, the endpoints that manage it and expose the binding on
/auth/me, the API rule that confines confirm-delivery to the dispatcher’s own station, the back-office tab where an admin assigns couriers, and the API tests that prove one station’s courier cannot touch another’s orders. On the device, a refusal by scope is deliberately distinguished from being offline — the difference between “you may not do this” and “try again later” decides whether the queue retries forever or stops.That boundary later had to reach the scanner itself. QR verification is offline, so a genuine QR from another service center scans clean and the courier learns it was never theirs only when the queue drains — after the fuel is gone. Both halves of the comparison were already on the device and had never met: the QR carries the order’s station,
/auth/mecarries the courier’s own. I addedlocation_idto the signed payload on the API side and the scan-time warning on the app side. Because the APK ships by hand there is no coordinated rollout, so the field is read as optional and the check is opt-in on its presence — the only shape that stays safe when the APK in someone’s pocket is older than the backend, and the reason it warns rather than blocks. -
Delivery evidence — a vertical slice of my own. Once the courier app was in the field, the product needed proof that a hand-off actually happened. I designed and shipped that across all three tiers: the FastAPI endpoints that attach photos to an order and expose the deliveries missing them, the back-office surfaces that review them, and the capture flow in the courier app — up to three photos per confirmation, later relaxed to optional. Most of the difficulty was in the offline semantics rather than the camera: queueing captures on a device that loses signal mid-shift, holding photos when the server refuses a delivery instead of dropping them, telling an authorization refusal apart from being offline, bounding upload attempts, and never leaving an orphaned photo on disk. A courier can also reopen an order already marked delivered to correct its evidence.
-
Surviving the white-label split. The team turned the platform into a white-label so one codebase could serve more than one operator — the client-application scoping, per-brand catalogues, email identity and deploys were other engineers’ work, not mine. What I owned was making my own surfaces survive the split: taking the courier app off the Mercaful name onto a brand-neutral mark (with the wordmarks drawn as widgets instead of shipped as SVG, so a future rename doesn’t mean re-cutting assets at every density), making the customer app name its own product per build, and rebuilding the legal documents for a multi-brand world: terms and conditions and the privacy policy now come from a single back-office-driven endpoint instead of two divergent paths, each brand can override any document from the back office, and shared clauses interpolate the site name and contact address so one text serves every operator without a copy-paste fork per brand.
-
Auditing my own app once it was already in the field. The app was in use and nothing was visibly wrong, which is exactly when a security surface stops getting looked at. I went back over mine anyway ahead of the 1.3.0 release. The worst of what I found was the PIN lock: five wrong attempts cleared the PIN hash and nothing else, leaving a setup screen that asks for no proof of identity and hands the live session to whoever picks the new one. Failing the lock was the way in. The counter also lived in widget state, so a relaunch returned a fresh set of guesses at a four-digit secret; it now lives in secure storage, and exhausting it clears the tokens outright — the guarantee no longer depends on me remembering to look. The same pass reordered non-atomic token writes, kept an empty
/public-keysresponse from wiping the keys that make offline QR verification possible at all, collapsed concurrent 401s onto a single renewal, and took customer data out of Android’s cloud backup. -
Shipping the app is part of the app. MyC Dispatcher goes out as a direct APK — no store, no staged rollout, couriers install what they are handed. Nothing re-signs the build downstream and no support channel can replace a lost key, so whatever signs the first install signs every update to it forever. Releases sign with a project keystore generated by a tool I wrote, and the release tasks fail at configuration time when the credentials are missing, so a build cannot quietly go out signed by something else. The same pass took
x86_64out of release builds — 19.5 MB of a 58.7 MB APK, for a set of real devices that is empty — which every courier had been downloading over a Cuban connection. -
What the app tells the operator. The home screen showed a bare count under “pending sync” — a number that could equally mean work still to do or deliveries already made waiting for signal. It now narrates five states across both queues, with the last successful sync persisted so it survives a relaunch, and offline painted neutral rather than danger: in an offline-first app offline is the normal operating condition, and red teaches the courier that the expected case is a broken one. The same principle fixed a notice that made couriers believe an early customer could not be served, and put the app’s colours on a semantic ramp aligned to the back office — with the status badges held a rung darker, because emerald-600 on white measures 3.77:1, below AA for a solid fill with white text.
-
Testing. I built out the Playwright end-to-end suite — a Page-Object-Model structure with
data-testid-only selectors and shared fixtures — covering the customer flows against the real backend. On the courier app the tests carry the rules a review would miss: a schema test that pins every column, so collapsing the migration history cannot silently drop one, and a sweep of every screen for English in user-facing strings — which is howSin appointmenthad survived, along with three sites rendering a raw Dart exception straight to the operator.
Outcome
The platform is built as a two-sided product, and both brands are in production and selling. The contribution I own outright is the courier app — a component I identified a better approach for, proposed, and delivered end to end: offline-first, in a stack I selected, with a design I authored. On the customer web app my role was that of a high-output contributor rather than the lead: I delivered a substantial share of the interface and connected it to the backend team’s external fuel-system integration, working under the lead frontend engineer and a designer who set the direction.
What began as a frontend role widened as the product did. The later slices — delivery evidence, the dispatcher-to-servicentro authorization boundary, the legal-document system — were built through the FastAPI backend and the back office as much as through the app, because a rule that only exists on the device is not a rule. The station check is the clearest case: I wrote the app half and the API half of the same signed payload, and the part that took the thinking was neither — it was deciding what an APK already in a courier’s hand may assume once the backend has moved on without it.
The strongest signal about the platform came from outside my own work: the team white-labelled it, and the same codebase now serves a second fuel brand, Gasolero, with its own domain, palette and support identity. I didn’t build that scoping layer — other engineers did. But a shared codebase only splits cleanly if every surface stops assuming one brand, and the surfaces I was responsible for — the courier app and the legal documents — were mine to take off the Mercaful name.
That distinction reflects how I operate within a team — executing at volume against a defined specification when that is the task, and taking genuine ownership when I identify something the team should approach differently. Ownership also means going back over your own code looking for what you got wrong: the way past the PIN lock was mine to write, and mine to find before anyone else did. On an app nobody else was reviewing, that habit is the only thing standing between a mistake and a shipped one.