Delivered remotely for a business based in Rome, Italy. Names withheld by agreement.
Project Overview
Industry: Consumer travel and mobility — road-trip planning, en-route discovery and driver navigation assistance.
Type of solution: A subscription consumer mobile product: two independently built native applications over a shared REST API, with a server-side administrative back office and a proxied third-party mapping layer.
Business context: Long-distance drivers are not neutral about where they stop. Loyalty programmes, rewards cards, fleet accounts, charging network subscriptions and simple familiarity mean most people have a short list of brands they will choose over any alternative — and a general-purpose map treats all of them as equivalent results. The gap between "somewhere to stop" and "the place I would actually choose" is filled today by the driver pulling over to search, or by detours discovered too late to take.
General users: Individual drivers planning and running multi-stop road trips, plus platform administrators managing the brand catalogue, content and monetisation.
General purpose: To turn route planning from a list of directions into a route the driver can actually use — with their own preferred hotels, restaurants, fuel, charging and rental brands surfaced along it, and a single tap to widen the search when those brands are not available.
The Business Challenge
Nearby is not the same as preferred. A driver's real question is not "what is around me" but "where is my brand, on my road, in the next two hours". Answering that requires the application to know something a mapping provider does not: which brands this particular person cares about.
Brands are not tidy data. Business names returned by a places provider are free text. A brand may operate under several names, may include a franchise or location suffix, and may belong to a portfolio where the parent name is the one the customer recognises. Matching a user's stated preference to a real-world listing is genuinely fuzzy work.
A trip is an object, not a query. A road trip is planned days before it is taken, edited repeatedly, reordered by hand, started, paused, resumed hours later and finished. That requires a persistent, ordered, editable itinerary that survives across sessions and devices — not a one-shot route calculation.
Ordering has to be exact. Stops are inserted in the middle, deleted, and dragged into new positions. Every one of those operations has to leave the itinerary in a consistent, gap-free order, from two different client implementations acting on the same data.
Third-party mapping is a metered cost. Nearby search, place detail and directions calls all carry a per-call charge, and the natural interaction model — search as the driver explores the map — makes call volume a function of user enthusiasm. Without control, engagement and cost rise together.
Credentials on a device are credentials in public. Mapping platform keys shipped inside a mobile binary can be extracted and used by anyone, at the account owner's expense.
Two stores, two different truths about who has paid. Subscription entitlement has to be verified against the platform the purchase was made on, kept current as renewals and cancellations occur outside the application, and reconciled when the same store subscription reappears under a different account.
Two platforms, one product. The map interaction at the centre of this application is not trivial, and it had to feel like the same product whether built in Kotlin or Swift.
Our Approach
Model preference as a ranked, grouped catalogue rather than free text. Rather than letting users type brand names, we built a curated brand catalogue per category, maintained in the back office with bulk spreadsheet import, and let users select a small ranked set from it during onboarding. Because brand portfolios rarely map one-to-one to customer-facing names, the catalogue supports preference groups — one user-facing selection can expand to a family of related brands. Catalogue entries also carry per-platform visibility, so the two stores can be given different lists where store policy or partner arrangements require it.
Put every third-party mapping call behind the API. The clients never hold the mapping credential for server-driven lookups. They send a relative provider path and parameters to a single API endpoint; the server attaches the credential, forwards the request, records the call against the user and platform, and returns the response. That gives credential containment, a usage ledger the business can actually read, and one place to enforce a throttle — three problems solved by one decision.
Treat itinerary order as server-owned. Stop ordering is enforced by the API, not by whichever client happened to write last. Inserting a stop at an occupied position cascades the conflicting entries upward; deleting a stop re-indexes the remainder. Both clients offer drag-and-drop reordering and submit the resulting sequence for the server to apply as a unit.
Distinguish rather than filter. Showing only preferred brands produces empty maps in unfamiliar territory, which is exactly when the driver most needs an answer. Instead, preferred brands and everything else are both available, rendered with visually distinct map pins, with per-category layer toggles and a single control to widen the search to all providers — plus an explicit prompt when a search returns none of the user's brands in that area.
Verify entitlement on the server, on both platforms. Purchases are made on-device through the native store flows, then posted to the API and verified server-side against the relevant store — including automatic fallback to the store's sandbox environment during testing. Store-to-server lifecycle notifications are consumed and persisted so that renewals, expiries and cancellations occurring outside the application are reflected without the user having to open it.
Handle the subscription that moves accounts. When a store subscription is presented under an account different from the one that previously held it, the earlier account's entitlement is withdrawn. This is a small piece of logic and an expensive one to omit.
Build both clients natively. Two native codebases, each using its platform's own mapping, billing, sign-in and advertising SDKs directly, with the API as the single shared contract.
The Solution
Ranked brand preferences across five categories. Lodging, restaurants, fuel, EV charging and vehicle rental, each with a small ranked set of preferred brands chosen during a staged onboarding flow and editable at any time.
Preference groups. A single selection can represent a family of related brands, so users pick the name they recognise rather than enumerating a portfolio.
Multi-stop trip planning. Origin, destination and an ordered set of intermediate stops, with route calculation, waypoint routing and route rendering on the map.
Drag-and-drop itinerary editing. Stops are reordered by hand on both clients, with the resulting order applied server-side as a consistent sequence.
Trip lifecycle. Trips move from planned to started to completed, with current position updated as the trip runs so a partially completed trip resumes correctly hours or days later.
Trip notes and per-stop comments. Free-text planning notes attached to the trip and to individual stops.
Route-corridor discovery. Tapping the map searches the surrounding area for places in the enabled categories, classifying results into the user's preferred brands and everything else, rendered with distinct pins.
Layer control. Per-category toggles plus a global control that widens the search from preferred brands only to all providers, with an explicit prompt when no preferred brands are found nearby.
Place detail and navigation handoff. Full place detail on tap, address lookup on long press, and handoff to the device's native navigation application to actually drive the leg.
Choice of map provider on iOS, with a default the user sets themselves, and spoken step-by-step guidance on the alternative provider.
Subscription access. Monthly and annual auto-renewing subscriptions purchased through the native store flows on each platform, verified server-side, with entitlement kept current from store lifecycle notifications.
Multiple sign-in paths. Email and password, three social providers, and Sign in with Apple, alongside password reset, email change and full account deletion.
Advertising with consent management. In-app advertising on both platforms behind the standard consent framework, plus a placement served from the back office.
In-app feedback and store review prompts. Rated feedback captured in-app and reviewed in the back office, with native review prompts surfaced at appropriate moments.
Lifecycle marketing synchronisation. User profiles, preferences and subscription state are synchronised to a third-party marketing-automation platform on a schedule, with every request and response recorded and viewable in the back office so failures surface rather than disappear.
Administrative platform. Brand catalogue management with spreadsheet import, user and trip inspection including a map view of any trip, content pages consumed by both clients, advertising, settings, feedback, subscription review, notification composition and the marketing synchronisation log.
Key Features
Ranked brand preferences with grouping Users declare a small ranked set of preferred brands in each of five categories, drawn from a curated catalogue that supports grouping so one selection can stand for a family of related brands.
Preferred-brand distinction along the route Discovery results are separated into the user's brands and everything else, drawn with distinct map pins, so the map answers "where is my brand" rather than "what is nearby".
Server-side mapping proxy with usage logging and throttling All server-driven mapping and places traffic runs through the API rather than from the device, keeping the credential off the client and producing a per-user, per-platform record of every call — with a single place to enforce a limit.
Server-owned itinerary ordering Stop order is enforced by the API with cascading reordering on insertion and re-indexing on deletion, so drag-and-drop editing from two independent clients cannot corrupt the sequence.
Trip lifecycle with resumable current position Trips move through planned, running and completed states, with position updated as the trip progresses so a multi-day journey resumes accurately.
Cross-store subscription verification Purchases are verified server-side against each platform's own verification model, with sandbox fallback for testing and store lifecycle notifications consumed so entitlement stays current without the user opening the app.
Subscription transfer handling When the same store subscription is presented under a different account, the previous account's entitlement is withdrawn — preventing a single purchase from entitling several accounts.
Bulk brand catalogue management The brand catalogue is maintained by non-technical staff through spreadsheet import with idempotent matching, and entries carry per-platform visibility flags.
Dual map provider on iOS with spoken guidance Users select their preferred mapping provider, with turn-by-turn step narration available on the alternative provider.
Auditable marketing synchronisation Every outbound profile synchronisation to the marketing platform is logged with its request and response and surfaced in the back office, so a failed sync is visible rather than silent.
Technical Architecture
Mobile clients. Two independently built native applications sharing no code. The Android client is Kotlin with an MVVM structure, dependency injection, coroutines and a typed HTTP layer with a bearer-token interceptor. The iOS client is Swift and UIKit with storyboards, a shared request manager and a slide-menu container. Both integrate their platform's native mapping and places SDKs, billing framework, sign-in providers, advertising SDK with consent management, and analytics and crash reporting.
API layer. A token-authenticated REST API over OAuth2 bearer tokens, with controllers partitioned into consumer, administrative and authentication trees over a shared base controller that standardises the response envelope across every endpoint.
Mapping proxy layer. A single server-side endpoint through which the clients route their provider calls. It attaches the credential, records the call against the user and platform, applies a throttle and returns the provider response unmodified.
Subscription layer. Platform-specific verification — a receipt posted to one store's verification service with sandbox fallback, and an OAuth-authenticated purchase lookup against the other store's developer API — converging on a single entitlement flag mirrored onto the user record, kept current by a store-to-server notification endpoint that persists both the raw and decoded payloads.
Data layer. A relational database holding users and preferences, trips and their ordered stops, the brand catalogue and its groupings, subscription and notification history, mapping call records, content pages, advertising and settings — evolved through a migration history with dedicated indexing migrations on the high-traffic tables and model-level caching on the catalogue.
Administrative back office. Server-rendered within the same application, covering catalogue, users, trips, content, advertising, subscriptions, feedback, notifications and integration logs.
Supporting services. Cloud object storage for user and advertising imagery with a local-disk fallback path, scheduled synchronisation to a marketing-automation platform, push notification delivery, transactional email, and continuous deployment driven from the hosted CI service.
Flow: Native mobile client (Android / iOS) → Token-authenticated REST API → Mapping and places proxy with usage logging → Third-party mapping platform → Relational data layer (trips, ordered stops, brand catalogue, entitlements) → Store verification and lifecycle notifications → Scheduled marketing synchronisation
Technology Stack
| Category | Technology |
|---|---|
| Backend language | PHP |
| Backend framework | Laravel |
| Database | MySQL with Doctrine DBAL and a migration-managed schema |
| API authentication | OAuth2 bearer tokens (Laravel Passport) |
| Query caching | Model-level caching on catalogue reads |
| Object storage | Google Cloud Storage with local-disk fallback |
| Mapping and places | Google Maps Platform — Maps, Places, Directions and Nearby Search, proxied server-side |
| Spreadsheet import | Maatwebsite Excel |
| Image processing | Intervention Image |
| SMTP and transactional email providers | |
| Content security | Spatie CSP with a hand-written policy |
| Web delivery | Progressive Web App support, Bootstrap admin theme, DataTables |
| CI/CD | Hosted CI pipeline driving Capistrano deployment |
| Android language | Kotlin on a modern Android Gradle Plugin and Java 17 toolchain |
| Android architecture | MVVM with ViewModel and LiveData, Koin dependency injection, coroutines |
| Android networking | Retrofit and OkHttp with a bearer-token interceptor |
| Android platform SDKs | Google Maps and Places SDKs, Maps Utils, Play Services Location, Play Billing, Play In-App Review |
| iOS language | Swift with UIKit, storyboards and XIBs |
| iOS networking | Alamofire with SwiftyJSON |
| iOS platform SDKs | Google Maps and Places SDKs, MapKit with speech synthesis, StoreKit |
| Identity | Email and password, Google Sign-In, Facebook Login, Sign in with Apple with Keychain storage |
| Monetisation | Native store subscription billing on both platforms; Google Mobile Ads with the UMP consent framework |
| Analytics and monitoring | Firebase Analytics and Crashlytics, server-side log viewer |
| Lifecycle marketing | Scheduled synchronisation to a third-party marketing-automation platform with per-request audit logging |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| A driver's real question is "where is my brand", not "what is nearby" | A curated per-category brand catalogue with grouping support, from which users choose a small ranked set, and a discovery layer that separates preferred brands from everything else with visually distinct map pins rather than filtering the map down to nothing. |
| Business names from a places provider are free text and brands operate under multiple names | Preference groups let one user-facing selection expand to a family of related brands, and the catalogue is maintained by non-technical staff through idempotent spreadsheet import so it can be corrected and extended without a release. |
| Metered third-party mapping costs that scale with user enthusiasm | All server-driven provider traffic runs through a single API proxy that records every call against the user and platform and applies a throttle, turning an invisible variable cost into an observable, controllable one. |
| Mapping credentials cannot safely live on a device | The proxy holds the credential server-side, so server-driven lookups never require the client to carry it. |
| Two independent clients editing the same ordered itinerary | Stop ordering is owned by the API — insertion at an occupied position cascades conflicting entries upward, deletion re-indexes the remainder, and drag-and-drop reordering is submitted as a sequence applied as a unit. |
| A trip spans days and must resume mid-journey | Trips carry an explicit lifecycle state and a current position updated as the journey progresses, so a partially completed itinerary resumes accurately rather than restarting. |
| Two app stores with entirely different models of who has paid | Platform-specific server-side verification — receipt verification with sandbox fallback on one store, OAuth-authenticated purchase lookup on the other — converging on a single entitlement flag, kept current by consuming store lifecycle notifications rather than waiting for the user to reopen the app. |
| One store subscription appearing under several accounts | Subscription identity is tracked by the store's own original transaction identifier, and presenting it under a new account withdraws the entitlement from the previous one. |
| Outbound marketing synchronisation failing silently | Every synchronisation request and its response are recorded and surfaced in the back office, with batch processing in chunks and internal accounts explicitly excluded. |
Security & Reliability
Token-based authentication. OAuth2 bearer tokens issued on sign-in, with registration, password reset, email change and account deletion flows, and multiple identity providers including Sign in with Apple.
Credential containment for mapping. Holding the mapping credential server-side and proxying provider traffic keeps it out of distributed binaries for all server-driven lookups, and gives a single point at which usage can be observed and limited.
Server-side entitlement verification. Subscription state is never trusted from the client. Purchases are verified against the issuing store, and lifecycle notifications are persisted in both raw and decoded form so entitlement history is reconstructable.
Entitlement exclusivity. Tracking store subscription identity prevents a single purchase from entitling multiple accounts.
Content security policy. An explicit, hand-written policy governs which sources the administrative interface may load from.
Integration observability. Outbound marketing synchronisation and inbound mapping calls are both recorded as structured data, so integration failures and usage anomalies are visible rather than inferred.
Crash and analytics instrumentation on both mobile platforms, with server-side log inspection.
Consent-managed advertising. Advertising on both platforms runs behind the standard consent framework rather than being served unconditionally.
Scalability & Performance
Cached catalogue reads. The brand catalogue is read on nearly every screen and changes rarely, making it the natural candidate for model-level caching.
Targeted indexing. Dedicated migrations add indexes to the tables carrying the highest read volume as the schema has grown.
Throttled, logged external calls. The mapping proxy bounds third-party call volume and makes it measurable, which matters more than raw throughput in a product whose principal marginal cost is per-call.
Chunked outbound synchronisation. Profile synchronisation to the marketing platform processes in batches rather than loading the full user set.
Externalised media. User and advertising imagery is stored in cloud object storage rather than on the application server, keeping the application tier stateless and horizontally scalable.
On-device rendering. Route decoding, polyline rendering and marker management run on the device using the platform mapping libraries, keeping the server out of the interactive rendering path.
Native clients. Building natively on each platform gives direct access to platform mapping, location and billing frameworks, with baseline profiles generated for the Android release to improve startup.
Business Outcomes
- Route planning answers the question drivers actually ask — where the brands they use sit along the road ahead, rather than what happens to be nearby.
- Preferences are captured once and reused everywhere, with grouping so users select the brand name they recognise rather than enumerating a portfolio.
- Itineraries survive real use — inserted, deleted and reordered stops leave a consistent sequence, and a multi-day trip resumes mid-journey.
- Third-party mapping cost became visible and bounded rather than an invisible variable that scales with engagement.
- Subscription entitlement is verified, current and exclusive, reflecting renewals and cancellations that happen outside the application and preventing one purchase from covering several accounts.
- The brand catalogue is a business asset, not a code change — maintained by non-technical staff through spreadsheet import, with per-platform control.
- Integration failures surface as data, so a marketing synchronisation problem is seen in the back office rather than discovered through its absence.
- One product across two platforms, each built natively against its own mapping, billing and identity frameworks over a single shared API contract.
Why it worked
Consumer applications built on metered third-party data have a failure mode that is easy to miss during a demo: the more people enjoy the product, the more it costs to run, and by the time anyone notices, the usage pattern is baked into the interaction design. The right time to decide where the provider credential lives, who is allowed to call the provider and how many times, and where those calls get recorded, is before the first map screen is built.
Our team builds for that reality. We put the third-party mapping layer behind the API from the start, so the credential stayed off the device, the call volume became a number someone could look at, and the throttle had one place to live. We made the itinerary's ordering the server's responsibility rather than a race between two clients. We verified subscription entitlement on the server against both stores, consumed the lifecycle notifications so the state stays true between sessions, and handled the case where a purchase changes hands. And we made the brand catalogue — the thing the product's entire value rests on — editable by the people who understand the brands, without a release.
Our teams work across Laravel and modern PHP, native Android in Kotlin and native iOS in Swift, mapping and location platforms, cross-store subscription commerce, and third-party integration pipelines — with the judgement to know which decisions are cheap now and expensive later.
Final Summary
A driver on a long trip is not looking for somewhere to stop. They are looking for the hotel chain where their points are, the restaurant they know, the fuel network on their card, the charging network their car and subscription are set up for. General mapping applications treat all of those as equivalent search results, and the gap gets filled by pulling over to search, or by a detour spotted too late.
Our team built a cross-platform road-trip planning application that closes that gap. Drivers declare a small ranked set of preferred brands across lodging, restaurants, fuel, charging and rental during onboarding, drawn from a curated catalogue that supports brand families. From then on, every route they plan distinguishes their brands from everything else on the map, with per-category layers and a single control to widen the search when their brands are not around. Trips are persistent, ordered, editable objects with a real lifecycle — planned at home, edited by hand, started, resumed mid-journey and completed.
Underneath, three decisions carry the product. All server-driven mapping and places traffic is proxied through the API, keeping the provider credential off the device and turning a metered, engagement-driven cost into something logged, bounded and observable. Itinerary ordering is owned by the server, so drag-and-drop editing from two independently built native clients cannot corrupt the sequence. And subscription entitlement is verified server-side against both app stores, kept current from store lifecycle notifications and made exclusive to one account at a time. Around that sit a brand catalogue maintained by spreadsheet rather than by release, an administrative platform covering content, advertising, feedback and integration logs, and audited synchronisation to a marketing platform so nothing fails quietly.