Delivered remotely for a business based in Copenhagen, Denmark. Names withheld by agreement.
Project Overview
Industry: Fuel retail and on-demand mobility services.
Type of solution: A two-sided marketplace and field-service workflow platform: a token-authenticated REST API with an administrative back office, plus two independently built native mobile applications serving distinct user roles.
Business context: In many markets a driver does not pump their own fuel — an attendant does. That interaction is almost entirely unmanaged. The driver cannot tell which station is open, who is on duty, what today's price is per grade, or how long they will wait. The attendant has no way to be found, no way to prove what they delivered, and no way to be paid other than in cash. The station owner has no reliable view of who is working, what prices were posted or what was pumped. Every party is operating on trust and memory.
General users: Drivers, station attendants, station managers and owners who employ attendants, and platform administrators.
General purpose: To make attendant-served refuelling discoverable, priced transparently, evidenced end to end and paid for in-app — for all three parties at once.
The Business Challenge
Nobody can find anybody. A driver approaching a forecourt has no signal about availability. An attendant standing on that forecourt has no way of being discovered by the driver two streets away. The most basic function of a marketplace — matching — did not exist.
Prices change daily and are set locally. Fuel is priced per station, per grade, and moves often. Any platform that displays a price takes on responsibility for it being right, which means the price has to come from the station itself, be recent, and be defensible after the fact.
The transaction is unsupervised and it is about money. Service is delivered in under a minute, on a forecourt, between two people who do not work for the platform, and the thing being exchanged is fuel for payment. Disputes are inevitable. Without evidence, every dispute becomes one person's word against another's.
Cash removes the record. Paying an attendant in cash means there is no receipt, no history, no rating, no recourse and nothing for the station owner to reconcile.
Attendants are not interchangeable, and not everyone should be listed. An attendant represents a station and handles money. Letting anyone appear in a driver's search results is not acceptable, so onboarding needs identity checks and staged approval, and station owners need control over who works under their station.
The work happens standing outside, one-handed, in poor signal. Both sides of this product are used on a forecourt, not at a desk. Forms must be short, entry must be forgiving, and a submission interrupted by a dropped connection must not corrupt the state it was trying to set.
Two platforms, two teams, one contract. The driver and attendant experiences both had to be first-class on Android and iOS, built natively on each platform, and stay behaviourally identical against a shared API.
Our Approach
Make availability a deliberate act, not an assumption. An attendant does not appear to drivers because they installed the app. They appear because they went on duty — and going on duty means publishing today's price for every fuel grade in the same submission. Discovery, availability and price freshness are one action, so the platform can never show a driver a stale price from an absent attendant.
Require evidence at the moment it is cheapest to capture. The pump price sign is photographed at the moment prices are published. The vehicle's registration plate is captured when the vehicle is registered and again against the transaction. The pump reading is photographed at the moment of fulfilment. Each is required at the step where the person is already standing in front of the thing being photographed, so evidence costs seconds rather than being reconstructed later during a dispute.
Treat a forecourt submission as something that will be retried. The duty-on submission carries a client-generated idempotency key, freshly created for each attempt and explicitly never reused, so a request that succeeds on the server but fails on the way back cannot be double-applied when the user taps again.
Keep photographs off the application tier. Every image the product depends on is uploaded from the device directly to cloud object storage using short-lived scoped credentials, with only the resulting reference sent to the backend. Images are resized and recompressed on the device first. The API never carries a binary.
Model the fulfilment as an explicit sequence of states. Request raised, accepted by the attendant, driver arrived with grade and amount confirmed, attendant signalling readiness, fuel delivered with any add-on services, receipt issued, payment settled, rating captured — each transition notified to the other party by push, so the two people involved stay synchronised without either having to watch a screen.
Gate who can be found. Attendants and stations enrol through a staged pipeline — email verification, then identity and document verification, then administrative enrolment approval — before they are discoverable. Station managers separately invite attendants to work under their station by email, with an explicit accept step, including a path for invitees who do not yet have an account.
Build natively on both platforms, and hold parity in writing. Android and iOS were each built as full native applications with platform-appropriate architectures. Where a contract changed, the change was specified in a written hand-off — the exact parameters, the validation order, the generation rules, the UX decisions — so parity was maintained by specification rather than by inspection.
Modernise iOS incrementally rather than rewriting it. New features are built as self-contained SwiftUI modules with their own view models and services, hosted inside the existing UIKit navigation, on top of a new async/await networking core — while the established screens continue to run untouched.
The Solution
Station and attendant discovery. Drivers see stations near them ordered by distance, each showing its current posted price per fuel grade and the attendants currently on duty there, with saved favourites for the places they return to.
Live price publishing. Attendants publish prices for every grade as part of going on duty, with a photograph of the pump price sign captured in that session. Previously submitted photographs cannot satisfy the requirement.
A vehicle garage with plate capture. Drivers register multiple vehicles with make, model, year, colour, fuel grade and registration plate — the plate captured by camera scan or entered manually, with an accompanying plate photograph used to identify the vehicle at the pump.
Routing to the forecourt. Turn-by-turn navigation to the selected station, so choosing an attendant and getting to them is one continuous flow.
Fill requests with a real workflow. A driver requests a fixed amount or a full tank against a specific vehicle and grade. The attendant accepts or declines. On arrival the driver confirms details; the attendant signals readiness; fuel is delivered with any add-on services recorded; a receipt is issued.
Photographic transaction record. The pump reading, the vehicle plate image and the posted price photograph are all bound to the transaction and visible on the payment detail screen alongside the purchase summary.
Cashless payment. Saved cards are tokenised on the device by a card tokenisation provider so no raw card data is retained by the platform, with a designated default card, a prepaid in-app balance as an alternative, optional tipping, and a full payment history grouped by date.
Ratings both ways. Drivers rate and review the attendant who served them; attendants see their own reviews and running average; administrators see top performers.
Staff and station management. Station managers enrol their station, set its profile, image and weekly opening hours, invite attendants by email with an explicit accept step, and manage their roster.
Promotions and notifications. Administrator-managed promotional campaigns with validity windows, and a typed push notification system that carries each workflow transition to the right party with the right deep link.
Administrative platform. A staged approval pipeline for attendants and stations, station and user management, fill request monitoring by status, activity and per-attendant revenue reporting, promotions, FAQ content and a support inbox.
Key Features
Duty activation coupled to price publishing An attendant becomes discoverable only by submitting current prices for every grade together with a freshly captured photograph of the pump price sign — making availability, price freshness and evidence a single action.
Idempotent state submission from unreliable connections A client-generated key, created per attempt and never reused, protects the duty-on transition against the double submissions that poor forecourt connectivity guarantees.
Multi-stage photographic verification Plate, pump price sign and pump record are captured at the moments they are cheapest to capture and bound to the transaction, so disputes are settled from evidence rather than recollection.
Distance-ordered discovery with live pricing Nearby stations ranked by distance, each showing current posted prices per grade and the attendants on duty right now.
Registration plate capture Camera-based plate scanning through a commercial OCR SDK, with manual entry and plate photography as the always-available path.
Explicit fulfilment state machine with push-driven coordination Every transition between driver and attendant is notified by push through a typed notification taxonomy, so both parties stay synchronised without watching a screen.
Tokenised card payments with a prepaid balance alternative Cards are tokenised on the device so no raw card data is retained, with a default card, an in-app balance, tipping and full payment history.
Direct-to-storage media pipeline Device-side resize and recompression followed by direct upload to cloud object storage with short-lived scoped credentials — no image ever traverses the API.
Staged attendant onboarding with employer-controlled rostering Email verification, identity and document verification and administrative enrolment approval gate discovery; station managers separately invite and manage the attendants working under their station.
Two-way ratings and reviews Driver ratings of attendants surfaced to the attendant, to the station and to platform administration.
Technical Architecture
Mobile clients. Two independent native applications. The Android client is Kotlin with MVVM — view models over a single networking repository, Retrofit with reactive streams, view binding, and platform SDKs for maps, storage, push and card tokenisation. The iOS client is Swift, running established UIKit screens alongside newer self-contained SwiftUI feature modules — each with its own view model, models, views and service — hosted inside the existing navigation stack.
iOS networking core. A purpose-built async/await client over the system networking stack: endpoint value types, a generic decoded response envelope, an auth interceptor that injects the token and the device parameters every call requires, typed error cases mapped from transport, HTTP, authorisation and decoding failures, and an in-memory request/response inspector for diagnostics. Models decode defensively so that loosely typed or not-yet-populated fields degrade the UI gracefully instead of failing it.
API layer. A token-authenticated REST API partitioned by audience — driver-side and attendant-side — behind an authentication middleware group, returning a consistent response envelope that both clients decode identically.
Administrative layer. A separately authenticated server-rendered back office with its own guard, covering the approval pipeline, catalogue and content management, and server-side paginated operational reporting.
Media layer. Device-to-cloud object storage upload using federated identity credentials issued per session, with only references stored in the application database.
Notification layer. A single dispatch helper composing typed notifications, persisting them for in-app history, and fanning out to both mobile platforms through their respective push services.
Data layer. A relational database holding users and roles, stations, daily price submissions, opening hours, vehicles, fill transactions, tokenised card references, wallet transactions, employment relationships, reviews, promotions and notifications — with hand-written SQL for geospatial ordering and operational reporting.
Delivery. Continuous integration driving a scripted release process with shared configuration, database migration and cache invalidation steps.
Flow: Native mobile client (role-specific) → Token-authenticated REST API → Business logic and notification dispatch → Relational database → Direct device upload to cloud object storage → Card tokenisation provider → Push notification delivery to both platforms
Technology Stack
| Category | Technology |
|---|---|
| Backend language | PHP |
| Backend framework | Laravel |
| Database | MySQL with Doctrine DBAL |
| API authentication | JWT bearer tokens for mobile; separate session guard for the back office |
| Admin interface | Server-rendered Blade templates with server-side paginated tables |
| Object storage | AWS S3 with Cognito identity-pool federated credentials |
| Push notifications | Firebase Cloud Messaging and Apple Push Notification service |
| Payments | Client-side card tokenisation with a tokenisation provider; prepaid in-app balance |
| SMTP delivery with templated transactional messages | |
| Monitoring | Framework introspection tooling and log inspection |
| Deployment | CircleCI driving a scripted release process with migration and cache tasks |
| Android language | Kotlin |
| Android architecture | MVVM with view models, LiveData and a single networking repository |
| Android networking | Retrofit with reactive streams and request logging |
| Android platform SDKs | Maps, cloud storage transfer, push messaging, card tokenisation, social sign-in |
| Android media | Image loading and caching libraries, cropping, camera capture |
| iOS language | Swift |
| iOS UI | UIKit with storyboards, plus SwiftUI feature modules hosted in the existing navigation |
| iOS networking | Purpose-built async/await client with endpoint descriptors, response envelope, auth interceptor and typed errors |
| iOS dependencies | Swift Package Manager for payments, push, maps and cloud storage SDKs |
| Mapping & routing | Maps, place autocomplete, place details and directions |
| Recognition | Commercial OCR SDK for registration-plate scanning |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| Displayed fuel prices going stale, and attendants appearing available when they are not | Availability and price publication were made a single action: an attendant becomes discoverable only by submitting current prices for every grade with a freshly captured pump-sign photograph in the same session, so a stale price and an absent attendant are structurally impossible to display. |
| An unsupervised, cash-replacing transaction with no record to settle disputes from | Photographic evidence required at each stage — plate, pump price sign, pump record — captured at the moment the person is already in front of the subject, and bound to a time-stamped transaction with a full state history. |
| Submissions retried over unreliable forecourt connections corrupting state | A client-generated idempotency key, created fresh for each submission attempt and explicitly never reused across retries or sessions, required by the server on the duty-activation transition. |
| Photograph-heavy workflows on an application tier that should not be moving binaries | Device-side resize and recompression followed by direct upload to cloud object storage under short-lived federated credentials, with only the resulting reference posted to the API — so image volume never touches the application tier. |
| Two people coordinating a sixty-second service from separate phones | An explicit fulfilment state machine where every transition dispatches a typed push notification to the counterparty, carrying its own deep-link target — so neither party has to watch a screen to know what happens next. |
| Two independent native codebases staying behaviourally identical on one API | Native builds on each platform with platform-appropriate architectures, and contract changes specified in written hand-off documents covering exact parameters, validation order, generation rules and UX decisions — parity by specification rather than by inspection. |
| Modernising a large established iOS application without a rewrite | A strangler-pattern migration: new features built as self-contained SwiftUI modules with their own view models and services on a new async/await networking core, hosted inside the existing UIKit navigation, with new services bridging into legacy components through continuations rather than replacing them. |
| A loosely typed API contract that both clients must consume safely | Defensive decoding on the client — numbers accepted whether typed or stringified, empty values treated as absent, and fields not yet populated by the backend modelled as optional so screens degrade gracefully instead of failing. |
| Not everyone who signs up should be visible to drivers | A staged onboarding pipeline — email verification, then identity and document verification, then administrative enrolment approval — before an attendant becomes discoverable, with station managers separately controlling who works under their station. |
Security & Reliability
Token-based API authentication. Bearer token authentication for both mobile clients with token refresh, applied through a dedicated middleware group, with the administrative back office authenticated separately under its own guard.
No raw card data retained. Card details are tokenised on the device by a tokenisation provider; the platform stores only a token, the card brand, expiry and last four digits, with an explicit default-card designation.
Scoped, short-lived upload credentials. Devices obtain federated credentials from an identity pool for object storage uploads rather than embedding long-lived keys, and upload directly to a scoped path.
Gated participation. Attendants and stations pass through email verification, identity and document verification and administrative enrolment approval before they can be discovered by a driver, with station managers controlling their own roster through explicit email invitation and acceptance.
Evidence retained per transaction. Plate imagery, pump price photographs and pump records are stored against the transaction, giving both parties and the platform a factual basis for resolving a dispute.
Complete transaction and state history. Every request, state transition, receipt, payment and rating is retained, so a transaction can be reviewed rather than reconstructed.
Operational visibility. Structured request and error logging with log inspection tooling, and framework introspection tooling available to the delivery team.
Payment guardrails. Ceilings applied to transaction and stored balance amounts, keeping individual exposure bounded.
Scalability & Performance
Binaries never touch the application tier. Photographs — the highest-volume data this product creates — are resized and recompressed on the device and uploaded straight to cloud object storage. The API handles references only, so image growth does not translate into application load.
Push instead of polling. Workflow transitions are delivered by push notification to the counterparty, so the coordination between driver and attendant does not generate a continuous stream of status requests from idle clients.
Stateless application tier. Token authentication and externalised media storage keep application instances free of local session state.
Paginated server-side operations. Administrative listings and reporting query and page at the database rather than loading full result sets, keeping the back office usable as transaction history accumulates.
Client-side caching and image handling. Both mobile clients use caching image pipelines, so repeated forecourt, vehicle and profile imagery is fetched once.
Bounded work on the critical path. The fulfilment flow is a short series of small state transitions, each doing minimal work, so the parts of the product used while someone is standing at a pump stay fast.
Scripted, repeatable releases. Continuous integration drives a release process with shared configuration, migration and cache invalidation, so deployments are consistent rather than manual.
Business Outcomes
- Attendant-served refuelling became discoverable, with drivers able to find on-duty attendants near them instead of guessing at a forecourt.
- Posted prices are current by construction, because publishing them is how an attendant becomes visible in the first place.
- The transaction has a record, replacing a cash exchange with an itemised invoice, a payment history and a photographic trail for both parties.
- Disputes have evidence to settle from, captured at the moment of service rather than reconstructed afterwards.
- Attendants have a reputation that follows them, through two-way ratings and reviews visible to drivers, employers and platform administration.
- Station owners gained visibility of their forecourt, including who is on duty, what prices were posted and what was pumped.
- Participation is controlled, with staged verification and approval before anyone becomes discoverable, and employer control over rostering.
- Both platforms are first-class, with native Android and iOS applications delivering the same behaviour rather than one leading and one following.
Why it worked
The hard part of a marketplace like this is not the software — it is that both sides of the transaction are standing outside, in a hurry, holding a phone in one hand, and neither of them will use anything that costs more effort than the cash exchange it replaces. Every design decision has to earn its place against that alternative.
That is the constraint we built to. We made publishing prices and going on duty the same action, because two separate steps means one of them gets skipped and the marketplace fills with stale prices. We required evidence at the moment the person is already looking at the thing being photographed, because evidence requested later never arrives. We put an idempotency key on the state transition that matters, because forecourt connectivity is bad and a user who taps twice should not corrupt their own state. And we kept every photograph off the application tier entirely, because the volume this product generates is images and that traffic should never have been the API's problem.
We also took the long view on the clients. Rather than rewriting a large, working iOS application, we built new features as self-contained modules on a modern async networking core inside the existing navigation — shipping value continuously while the architecture improved underneath. Where two independent native teams had to stay in step on a shared contract, we held parity in written specification rather than hoping inspection would catch the drift.
Our teams work across Laravel and modern PHP, native Android in Kotlin and native iOS in Swift, cloud storage and identity, payment tokenisation, push infrastructure, mapping and OCR integration — with the product judgement to recognise which constraints a real user in a real environment will actually accept.
Final Summary
A driver pulls onto a forecourt and someone else pumps their fuel. It is a transaction that happens millions of times a day and, until recently, left no trace: no way to find an available attendant, no published price you could rely on, no receipt, no rating, no record for the station owner, and cash at the end of it.
Our team built a platform that gives that transaction structure. Attendants become discoverable by publishing today's prices for every grade with a photograph of the pump sign captured in that session — so availability, price freshness and evidence are one act rather than three that could each be skipped. Drivers find nearby stations ranked by distance with live prices and on-duty attendants, navigate there, and request a fill against a registered vehicle. The fulfilment runs as an explicit sequence of states, each transition pushed to the other party, with the pump record photographed at delivery, an itemised invoice issued, and payment settled from a tokenised card or an in-app balance with optional tipping. Both sides rate each other afterwards.
Around that sit the things a marketplace between strangers requires: staged verification and administrative approval before anyone is discoverable, employer-controlled rostering, evidence retained against every transaction, payment ceilings, and a complete history that makes a disputed fill reviewable rather than arguable. It is delivered as two full native mobile applications — Kotlin on Android, Swift on iOS with a modern async core and an in-progress SwiftUI migration — over a Laravel API and administrative platform, with photographs going device-to-cloud and never through the API. The result replaces a cash handshake with a record, on a forecourt, in the time it takes to fill a tank.