Delivered remotely for a business based in Austin, United States. Names withheld by agreement.
Project Overview
Industry: Agriculture — precision irrigation, agricultural water management and field telemetry.
Type of solution: A cross-platform mobile application for industrial equipment monitoring and remote control, built over a remote telemetry and command service.
Business context: Broad-acre irrigated farming depends on machines that move continuously and slowly across large areas, frequently at night and often several kilometres from anywhere anyone is standing. Historically, knowing what those machines were doing meant driving to them, and changing what they were doing meant driving to them again. The cost of that is not only fuel and hours; it is the gap between a machine stopping and somebody noticing, during which an unwatered wedge of crop is quietly being lost.
General users: Growers and irrigation operators responsible for multiple machines across a wide area, and multi-property operators overseeing several farms from one account.
General purpose: To collapse the distance between the operator and the equipment — continuous visibility of every machine, immediate notification when status changes, and the ability to issue and schedule control instructions without attending the machine.
The Business Challenge
Equipment operates out of sight. Machine status, position on the arc, direction of travel, water pressure and end gun state are all invisible unless someone is physically present. On a property with machines spread across a wide area, a complete manual check consumes most of a working day.
Delay is the real cost of a fault. A stalled machine in hot weather damages crop continuously until it is restarted. The commercially significant number is not how often faults occur but how long they go unnoticed, which historically was "until the next drive-around".
Adjustment required attendance. Changing speed, reversing direction, stopping at a chosen point, or switching an end gun on and off meant reaching the machine's own control panel — often at night and in poor conditions.
Scheduling by clock is the wrong model. Growers do not think in terms of time; they think in terms of position. "Stop at the corner of the field", "keep the end gun off across the road frontage", "reverse when you reach the fence line". A machine travelling at variable speed around an arc arrives at a given position at a time nobody can predict in advance, so time-based scheduling forces the operator to do a conversion the software should be doing.
Different equipment, one operational picture. Irrigation machines, open-channel water infrastructure and tracked farm assets are all part of the same operation and need to appear in the same view, despite behaving completely differently and reporting completely different data.
Alerts must work when nobody is looking at the phone. The alert that matters most arrives in the middle of the night. It has to be audible, and it has to be distinguishable from every other notification the operator receives.
The authoritative state lives in the equipment, not in the app. A machine's real configuration is held by its controller in the field. The danger in a control application is not that a change fails, but that the operator believes it succeeded — so the system has to be explicit about what has actually reached the equipment and what has not.
Commands act on physical machinery. This is not a form submission. An instruction that is ambiguous, duplicated, silently dropped or inappropriate for the machine's current state has physical and financial consequences.
Our Approach
Model position, not time, as the scheduling primitive. We built command scheduling around the machine's angular position on its arc. An operator nominates a position and an action, optionally repeating on every revolution, and the platform holds the instruction until the machine arrives. Alongside it we built an arrival projection, so the operator can see when each queued instruction is expected to fire rather than having to estimate it.
Let the service decide what an operator may do. Rather than encoding control permissions in the app, the platform receives a per-site capability set and lock state from the service and renders controls accordingly. Control availability can change per machine, per property and per account without an app release, and the client never assumes a capability it has not been granted.
Make the first screen useful immediately. The estate summary is delivered as a stream rather than a single response, with incoming records batched and rendered as soon as a first group has arrived. An operator opening the app because a machine has stalled sees machines within moments instead of waiting for the whole property to load.
Use the right transport for each job. Streamed delivery for the initial load, a live subscription for updates while the operator is watching, conventional request/response for everything else, and push notifications for when the app is closed entirely. Each converges into the same client state.
Treat a command as a physical act. Every dispatched instruction requires explicit confirmation and returns explicit acknowledgement, so the operator always knows whether it left the phone. Where a command would be inappropriate for the machine's current configuration, the platform intervenes and offers the operator a choice rather than either failing silently or doing something unintended.
Make alarms identifiable by ear. Notification types can be assigned distinct alert sounds — deliberately including some that are hard to sleep through — delivered through dedicated notification channels, so an operator woken at 3am knows what kind of alert it is before picking up the phone.
Never let the operator act on state that might not have arrived. Where the authoritative state lives in field hardware, the risk is not that a change fails but that the operator believes it succeeded. Sector programmes edited on the device are tracked locally as unsent until they have been pushed to the equipment, and the control that applies them is visually flagged until then. Reference content, session state and preferences are cached so the application opens instantly, while live equipment status is always fetched rather than served from a cache that could quietly go stale.
Render the geometry honestly. An irrigation machine is an arc at a real-world radius with a direction of travel, not a pin on a map. We render it as such — arcs, direction indicators, partial-circle handling and sector programmes drawn in their true geographic position — with clustering so a large property stays legible.
The Solution
Live estate overview. Every machine and monitored site on the property in list, compact and grid presentations, with distinct renderers per equipment type so heterogeneous equipment shares one operational view.
Geographic operations map. Machines drawn as true geographic arcs with direction indicators and partial-circle handling, marker clustering for dense areas, detail callouts, saved favourite locations and persisted map preferences.
Weather and imagery layers. A rainfall map with clustered gauge readings, an animated weather radar overlay with a time control for reviewing recent frames, and satellite imagery of the irrigated area available per site.
Immediate remote control. Start, stop, forward, reverse, speed, automatic and manual mode, end gun control, chemical pump control and a status refresh — dispatched from the phone with confirmation and acknowledgement.
Position-scheduled commands. Stop, direction change, speed change, end gun and chemical pump changes, and position alerts, all queued against a nominated position on the arc and optionally repeated each revolution, with a graphical position selector.
Arrival projection. Queued commands are projected onto the map with expected arrival indicators, so the operator can see what will happen where, and roughly when.
Sector programmes. Named, reusable multi-row speed and end gun tables mapping sectors of the arc to specific speeds or end gun states, direction-specific, with a clear indication of which programme is currently running.
Automated oscillation. Direction programmes that reverse the machine automatically between two nominated angles with a configurable dwell, so a partial-circle machine can work a defined span unattended.
Command queue management. A consolidated list of pending scheduled commands, grouped for review, with removal of queued instructions before they fire and a per-machine hold that suspends the whole queue without deleting it.
Configurable alerting. Push notifications with separate day and night alert sounds per notification type, previewable in the app, each on its own notification channel — plus a permission state model that tells the operator plainly when alerting is not actually working, and a direct route into system settings to fix it.
Attention-ordered overview. Machines in a fault state are promoted to the top of the list with the elapsed duration of the fault, so the thing that needs attention is the thing the operator sees first.
Water infrastructure monitoring. Open-channel sites with level sensing, high and low alarm states, setpoints and difference charting, presented through their own detail screens.
Sensor charting. Pressure history with an adjustable low-pressure setpoint, and rainfall accumulation over weekly and yearly windows, with full-screen chart views.
Tracked assets. GPS-tracked farm assets as a first-class site type with their own markers, colour coding, movement history charts and detail views.
Multi-property accounts. An umbrella account owning multiple property groups, with switching between them and group-level settings, messaging and support contact.
Operational record. Per-site history, message history, recent messages and operator notes, so what happened to a machine is recoverable rather than remembered.
Unsent-edit tracking. Sector programmes edited on the device are recorded locally as pending until they have been pushed to the equipment, with the apply control flagged until they have.
Instant session restore. Cached session state and preferences bring the application straight to the operational view on launch, without waiting on a round trip.
In-app help. A searchable, locally cached help library with fuzzy matching and version checking, so reference material is available in the field, plus operator-facing broadcast messages with acknowledgement.
Field-readable display. A global font scale spanning half to double size, applied consistently across every screen through an in-house component layer — for reading a phone outdoors in direct sunlight.
Key Features
Position-based command scheduling Commands are queued against the machine's angular position on its arc rather than against a clock time, optionally repeating each revolution — matching how irrigation decisions are actually expressed.
Arrival projection for queued commands Pending positional commands are projected with expected arrival indicators, converting current speed and position into an estimate the operator would otherwise have to work out.
Server-declared control capability The service supplies a per-site capability set and lock state; the application renders only the controls it has been granted, so permissions change without an app release.
Progressive streamed loading The estate summary streams in and renders after the first batch of machines rather than after the last, so the app is useful within moments of opening.
True geographic equipment rendering Machines are drawn as arcs at their real-world radius with direction of travel and partial-circle handling, with clustering to keep large properties legible.
Multi-mode speed entry Speed can be entered as a percentage, as application depth or as time per revolution, with the other representations updating live — so the operator works in whichever unit they think in.
Sector programme tables Reusable multi-row programmes mapping arc sectors to speeds or end gun states, rather than a sequence of individual commands.
Distinguishable audible alarms, with day and night profiles Separate daytime and night-time alert sounds per notification type, each on its own notification channel, so an alert is identifiable by ear before the phone is picked up — and so an overnight alert can be made deliberately harder to sleep through than a routine daytime one.
Safety interlock on inappropriate commands Where a command would conflict with the machine's current operating configuration, the platform surfaces the conflict and offers the operator an explicit choice rather than acting on an ambiguous instruction.
Unsent-edit tracking for sector programmes Where the authoritative state lives in field hardware, locally edited programmes are tracked as pending and the apply control is flagged until the change has actually been pushed.
Technical Architecture
Mobile client. A substantial cross-platform application organised into roughly thirty domain state stores, a consolidated API interaction layer, a screen tree partitioned by scope — property level, site level, rainfall and help — and a component library of well over a hundred components with dedicated subtrees for equipment geometry rendering, control widgets and charting. Navigation is composed from native stack, bottom tab and drawer navigators.
State layer. Observable domain stores with computed projections drive rendering directly. Control is implemented as two parallel stores sharing one interface — immediate and position-scheduled — so a single set of control components serves both modes without duplication in the UI layer.
Transport layer. Three complementary channels feeding one state model: a server-sent event stream for progressive initial load with batched writes, a live socket subscription scoped to the active property for updates while the app is open, and conventional request/response for everything else. Push notification delivery covers the closed-app case. A singleton stream manager prevents duplicate concurrent connections.
Persistence layer. Key-value storage holds session state, preferences, cached reference content and acknowledgements, so the application opens directly into the operational view. A small embedded relational store serves a separate and deliberately narrow purpose: recording which sector programmes carry edits that have not yet reached the equipment. Live equipment status is always fetched rather than cached, so an operator is never shown stale machine state believing it to be current.
Geospatial layer. Map rendering with real-world arc and circle geometry for irrigation machines, direction indicators, sector overlays and projected command markers, supported by spherical geometry calculation, with marker clustering and outlier-filtered aggregation on the rainfall layer.
Presentation layer. A Material Design component system with vector iconography, native-driven animation and gesture handling, orientation control, keyboard management and an adjustable font scale for outdoor legibility.
Native layer. Fully native Android and iOS projects with platform build tooling, native push notification handling per platform, permission management, audio playback for alarms, location services, connectivity detection and store version checking.
Flow: Mobile client → streamed initial load + live subscription + request/response → remote telemetry and command service → field equipment controllers, with push notification delivery for closed-app alerting and local persistence for offline availability.
Technology Stack
| Category | Technology |
|---|---|
| Mobile framework | React Native with React 19, bare workflow |
| Language | TypeScript |
| State management | MobX with observable stores and decorator syntax |
| Navigation | React Navigation — native stack, bottom tabs and drawer |
| UI system | React Native Paper (Material Design), vector icons, Reanimated, Gesture Handler, Safe Area Context |
| Mapping | React Native Maps with marker clustering and spherical geometry calculation |
| Charting | React Native SVG with an SVG charting library and D3 scales |
| Local persistence | Asynchronous key-value storage for session and preferences; embedded SQLite for unsent-edit tracking |
| Accessibility | Global font scaling applied through an in-house component wrapper layer |
| Streaming | Server-Sent Events for progressive load |
| Live updates | Socket-based subscription client |
| Push notifications | Firebase Messaging with native platform notification handling and per-channel sounds |
| Crash reporting | Firebase Crashlytics |
| Location & permissions | Native location services with a unified permissions layer |
| Device & connectivity | Device information, network state detection, app store version checking |
| Media & display | Native audio playback for alarms, Markdown rendering, WebView, orientation locking |
| Search | Client-side fuzzy search for help content |
| Date & time | Moment with timezone and duration handling |
| Build & tooling | Gradle and CocoaPods native builds, Metro bundler, Babel with production console stripping, dependency patching, ESLint, Prettier, Jest |
| Previous generation | Managed cross-platform toolchain with over-the-air bundle publishing via CI, since migrated |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| Growers express irrigation instructions positionally, but scheduling systems are built around clock time | Position-based scheduling as the primary abstraction — commands queued against angular position on the arc with optional per-revolution repetition, plus an arrival projection converting current speed and position into an expected trigger time, so the operator never has to perform the conversion themselves. |
| An operator opening the app during a fault cannot wait for a full property to load | The estate summary is streamed rather than returned as one response, with incoming records batched and the first group rendered as soon as it arrives, so useful information appears in moments regardless of property size. |
| One dataset arriving over three different transports must converge without flicker or lost updates | Streamed load, live subscription and request/response all write into a single observable state model, with batched writes on the streaming path and a singleton stream manager preventing duplicate concurrent connections. |
| Control permissions differ per machine, per property and per account, and change over time | Capability is declared by the service as a per-site control set plus lock state, and the client renders only what it has been granted — so permission changes require no app release and the client never assumes an authority it does not have. |
| Irrigation machines are arcs in the real world, not pins on a map | Geographic rendering of true arcs at real-world radius with direction indicators, partial-circle gap handling and sector overlays drawn where they actually apply, with map interaction hit-tested against the real geometry so tapping anywhere within a machine's circle selects it rather than requiring a precise hit on a pin. |
| Commands act on physical machinery, where ambiguity has real consequences | Explicit confirmation before dispatch and explicit acknowledgement after it, so the operator always knows the state of their instruction, plus an interlock that surfaces conflicts between a command and the machine's current operating configuration and asks the operator to choose rather than guessing. |
| The alert that matters most arrives while the operator is asleep | Separate day and night alert sounds per notification type, each on its own notification channel with the channel selected server-side per message, deliberately including alarms that are difficult to sleep through — so an alert is identifiable by ear before the phone is picked up. Supported by an explicit permission state model that tells the operator plainly when alerting is not working, rather than failing silently. |
| The authoritative state of a machine lives in field hardware, not in the app | Programmes edited on the device are recorded locally as unsent and the apply control is flagged until the change has reached the equipment, so an operator is never left assuming a change took effect. Live status is always fetched rather than served from a cache, on the principle that showing stale machine state is worse than showing none. |
| A large domain codebase had to survive a full platform migration | The migration from a managed cross-platform toolchain to a bare native workflow preserved the domain layer — state stores, interaction layer and screen taxonomy — while replacing the navigation, mapping, notification, persistence and monitoring stacks beneath it, keeping years of accumulated domain logic intact across major framework versions. |
Security & Reliability
Service-governed control authority. What an operator can do to a given machine is declared by the service as a per-site capability set, not decided by the application. The client is a renderer of that authority rather than the author of it.
Layered control gating. Property-level and machine-level control locks operate alongside a control-hold state, giving several independent means of withdrawing control without withdrawing visibility — so an operator can retain monitoring on equipment they are not permitted to command.
Explicit command confirmation. Every dispatched instruction requires deliberate confirmation and returns an explicit acknowledgement, so a command is never issued accidentally and never silently lost.
Operating-state interlocks. Where an instruction conflicts with the machine's current operating configuration, the platform surfaces the conflict and requires an explicit operator decision rather than proceeding on an ambiguous instruction.
Remote revocation. Beyond rejecting individual requests, the live update channel carries authority changes: control can be withdrawn or a session invalidated from the service while the application is open, and the client acts on it immediately rather than at next launch.
Session invalidation. Rejected credentials trigger immediate sign-out and a return to authentication, so a stale session cannot linger against equipment control.
Operational record. Per-machine history, message history and operator notes provide a reviewable account of what happened and what was instructed.
Alert integrity. The notification permission state is modelled explicitly and surfaced to the operator, so a silently broken alerting path — the failure mode that matters most in this product — is visible rather than assumed to be working.
Production monitoring. Crash reporting is integrated on both platforms, with diagnostic output stripped from production builds and code minification applied to release artefacts.
No stale state presented as live. Equipment status is always fetched rather than served from a local cache, and locally edited programmes are explicitly marked as unsent until they have reached the equipment — so what the operator sees is either current or visibly flagged as pending.
Scalability & Performance
Streamed progressive loading. The estate summary is delivered as a stream with batched state writes and rendering triggered after the first group of machines, so time-to-useful-screen is largely independent of how many machines a property has.
Separated summary and detail. A lightweight overview payload is distinct from full machine detail, so the common case — checking the status of everything — does not carry the cost of the rare case.
Clustered aggregation on dense layers. The rainfall layer clusters its markers and aggregates readings statistically, bounding rendered map children and keeping pan and zoom responsive over wide areas.
Scoped live subscription. The live update channel is subscribed per property rather than globally, so an operator receives only the traffic relevant to what they are viewing.
Single-stream enforcement. A singleton stream manager prevents duplicate concurrent connections from repeated navigation, avoiding both redundant load and conflicting writes.
Staleness-triggered refresh. A full refetch is triggered only when data has aged past a threshold, rather than on every navigation, so incremental live updates carry the load in normal operation.
Version-checked reference caching. Help content is cached locally and refreshed only when the service reports a newer version, so reference material costs nothing to keep current.
Build-time optimisation. Diagnostic output removed from production bundles and code minification applied to release builds.
Native rendering paths. Animation and gesture handling run on native driver paths, and chart and geometry rendering is scoped to the visible site rather than the whole estate.
Business Outcomes
- Equipment status is continuously visible from anywhere, replacing physical inspection rounds as the primary means of knowing what machines are doing.
- The gap between a fault and awareness of it collapses, because status changes are pushed to the operator with identifiable audible alerts rather than discovered on the next drive-around.
- Control no longer requires attendance. Speed, direction, stopping, end gun and mode changes are issued from a phone instead of from the machine's own panel.
- Instructions can be expressed the way growers think about them — against a position on the arc rather than a time on a clock — with the platform handling the projection of when that position will be reached.
- Repeated irrigation patterns become reusable programmes through sector-based speed and end gun tables rather than sequences of manual interventions.
- Heterogeneous farm infrastructure shares one operational picture, with irrigation machines, water infrastructure and tracked assets in a single view.
- Operators are never left guessing whether a change took effect, because programmes edited on the device are visibly marked as unsent until they have reached the equipment.
- Control authority is administered centrally and can be withdrawn remotely, since capability and lock state are governed by the service, change without an application release, and take effect on open sessions immediately.
- The product survived a full platform migration from a managed toolchain to a native workflow, carrying its accumulated domain logic forward onto a current, maintainable foundation.
Why it worked
Software that operates physical machinery is a different discipline from software that manages records. A form submission that fails can be retried; a command sent to a machine in a field cannot be un-sent, and an instruction that is ambiguous about what and where is worse than no instruction at all. The engineering questions are consequently different ones: what is the authoritative source of what this operator may do, how does the operator know their instruction actually arrived, and what happens when the network drops halfway through.
Our team builds for that reality. We took the scheduling abstraction from the domain rather than from the framework, because growers reason about position on an arc and forcing them to reason about clock time would have made the product slower than walking out to the machine. We put control authority on the service and made the client a renderer of it, so that what an operator may do is administered rather than compiled in. We chose a different transport for each job — streamed, subscribed and requested — because a single one would have been wrong for at least two of them. And we treated offline as the normal case rather than the error case, because the operator is standing in a field.
We also brought a five-year-old application forward onto a current foundation without discarding what it knew. Migrating from a managed cross-platform toolchain to a fully native workflow, across major versions of the framework, the navigation layer, the mapping stack, the notification system and the persistence layer, while preserving a large and hard-won domain model, is unglamorous work that determines whether a mature product has another decade in it.
Our teams work across React Native and TypeScript, geospatial and mapping applications, real-time and streaming data delivery, embedded mobile persistence and offline-first design, native platform integration on both mobile platforms, and long-horizon platform migration — with the domain judgement to recognise when the obvious abstraction is the wrong one.
Final Summary
Irrigation machines work unattended, overnight, out of sight, and the cost of one stopping is measured in the hours before anybody notices. For decades the answer was to drive out and look, and to drive out again to change anything.
Our team delivered a mobile platform that closes that distance. Every machine on the property appears on a map as true geographic geometry — arcs at their real radius, with direction of travel and sector programmes drawn where they actually apply — alongside rainfall mapping, an animated weather radar overlay and satellite imagery of the irrigated ground. The estate summary streams in and renders after the first machines arrive rather than the last, so the app is useful within moments of being opened during a fault. Live updates arrive over a subscription while the operator watches, and push notifications with identifiable audible alarms reach them when the app is closed and they are asleep.
Control is issued from the phone: start, stop, direction, speed, mode, end gun and chemical pump, each requiring deliberate confirmation and returning explicit acknowledgement, with interlocks that surface a conflict rather than acting on an ambiguous instruction. The distinctive part is that commands can be scheduled against the machine's position on its arc rather than against the clock, repeating each revolution if required, with projected arrival indicators showing when each queued instruction is expected to fire. That single decision — taking the scheduling abstraction from the domain instead of from the framework — is what makes the product faster than walking out to the machine, which is the only benchmark that counts.
Around it sit the things a field tool genuinely needs: control authority declared by the service rather than compiled into the app and revocable remotely while a session is open, programmes marked as unsent until they have actually reached the equipment, one operational picture spanning irrigation machines, water infrastructure and tracked assets, a display readable in direct sunlight, and a reviewable record of what happened and what was instructed. Delivered as a native cross-platform application, migrated intact from an ageing managed toolchain onto a current foundation with its accumulated domain knowledge preserved.