Building OnSite Tracker: GPS, Noise, and the Engineering of Fair Pay
A solo, end-to-end build — a GPS field-staff tracker that turns messy phone location data into fair petrol allowances and honest billing. The full-stack story: noise filtering, snapshotted rates, offline resilience, and shipping a real product alone.
Namith K P
· 26 min read
OnSite Tracker is a product I designed, built, and shipped entirely on my own — and it's live, in production, used to pay real people. It solves an unglamorous but very real problem for service businesses: their staff travel to job sites, and the business needs to fairly compensate that travel (a petrol allowance based on distance) and accurately bill the time spent working on site. Done manually, this is a mess of guesswork, honesty-system logbooks, and disputes. OnSite Tracker turns it into software.
This post is the full engineering story — not a tools list, but the actual problems that made it interesting: turning noisy GPS into a trustworthy distance number, designing a rate system that keeps history honest, surviving the mobile networks of the real world, and what it's like to own every layer of a product alone. It's the on-brand sequel to a much shorter post I once wrote about my "frontend workflow" — because the truth is my workflow was never really front-end. It's whatever the product needs, top to bottom.
Why I'm drawn to unglamorous problems
Before the technical story, a word on why I built this at all, because it says something about the kind of engineer I'm trying to be. Field-staff travel reimbursement is not a glamorous domain. There's no AI hype in it, no impressive demo that makes a room gasp. It's the sort of back-office operational problem most engineers would find beneath them.
I find it fascinating, and I've come to believe the unglamorous problems are often the most worth solving. They're unglamorous precisely because they're unsolved — real, persistent friction in how actual businesses operate, the kind of thing people put up with because "that's just how it is." Software that removes that friction creates disproportionate value, not because it's technically dazzling, but because it takes a genuine daily pain and makes it disappear. A business that stops arguing about petrol allowances and stops manually reconciling logbooks got something real from software, even if that software will never trend on tech Twitter. I'd rather build the thing people quietly depend on than the thing people briefly admire.
The problem, precisely
A service business — think a company whose technicians drive to customer sites — has two things it needs to get right about field staff, and both are money.
First, travel compensation. Staff use their own vehicles to get to jobs, and the business reimburses them per kilometre (a petrol allowance). To do that fairly, you need to know how far each person actually drove for work. Ask them to log it themselves and you get a spectrum from honest estimation to optimistic fiction, plus the friction of people having to remember and record every trip.
Second, billing for on-site time. The business bills customers for the hours worked on site, so it needs an accurate record of when work started and stopped at each location. Again, the manual version is a logbook and a lot of trust.
Both problems share a hidden third cost, too: the friction of the manual system. Even when everyone is honest, asking people to remember and log every trip and every on-site stretch is a tax on their time and attention, and reconciling those logs is a tax on the office. The disputes that inevitably arise — "I'm sure I drove further than that" — cost goodwill on top of time. A good system doesn't just make the numbers accurate; it removes the whole burden of producing them, so nobody has to think about it at all.
Both problems have the same shape: a real-world physical activity (driving, working) that needs to become a trustworthy number (kilometres, hours) that money is calculated from. That "physical reality to trustworthy number" translation is exactly the kind of problem I find irresistible, and it's much harder than it first sounds — because the raw signal you get from a phone is noisy, and money calculated from noisy data is money calculated wrong.
Why GPS is harder than it looks
The naive mental model is: the phone knows where it is, so recording a trip is just adding up the distance between successive GPS points. If that were true, this would be a weekend project. It isn't true, and the gap between the naive model and reality is where most of the actual engineering lives.
Phone GPS is noisy in specific, money-wrecking ways. A stationary phone doesn't report a single fixed location — it reports a little cloud of points that jitter around the true position, because GPS accuracy is finite and drifts with conditions. So a technician parked at a job site for three hours, phone sitting still on the dashboard, generates a stream of slightly-different locations. Sum the naive distances between them and you've "driven" several kilometres without moving an inch. Multiply that by every stop, every day, every staff member, and the petrol allowance is quietly, systematically wrong — the business overpays, and worse, the number nobody can trust undermines the whole point of the system.
It gets worse. GPS occasionally produces wild outliers — a single reading that places you a kilometre away for one instant before snapping back, usually from a momentary bad fix in an urban canyon or under cover. Naively, that's a two-kilometre round trip in one second: an impossible 7,000 km/h journey that adds phantom distance to the total. Buildings, tunnels, and weather all degrade accuracy in ways that inject more noise. The raw GPS stream is not a clean record of movement; it's a smeared, occasionally-lying signal that you have to interpret carefully before you dare calculate money from it.
Filtering the noise, server-side
The heart of OnSite Tracker's backend is the filtering that turns that smeared signal into a trustworthy distance. I made a deliberate architectural decision to do this filtering server-side, not on the phone, and that choice matters: the client's job is to faithfully capture and report what the GPS says, and the server's job is to be the single, authoritative place where raw points become a trusted number. Centralizing the trust logic means the rules are consistent for everyone, auditable, and improvable without shipping a new app to every phone.
The filtering works on a few complementary principles. It discards low-accuracy points — every GPS reading comes with an accuracy estimate, and a point the device itself says it's unsure about shouldn't be trusted to add distance. It suppresses parked drift — when the movement between points is within the range of GPS jitter rather than real travel, that "movement" is noise, and it gets treated as standing still rather than as distance driven. And it rejects physically impossible jumps — if getting from one point to the next would require an implausible speed (the >150 km/h outliers), the point is an error, not a journey, and it's thrown out rather than allowed to poison the total.
The result of all this is a distance number the business can actually stand behind: it reflects real driving, not the phantom kilometres of jitter and the impossible leaps of bad fixes. Getting this right was the difference between a product that pays people fairly and a product that's confidently wrong — and "confidently wrong" is the worst thing a system that calculates money can be, because people trust it precisely when they shouldn't.
A note on where filtering lives
It's worth being explicit about why server-side filtering was the right call rather than just an arbitrary one, because it's a decision with real consequences. If the filtering lived on the phone, every device would be its own little authority on what counts as "real" distance, and improving the rules — as I inevitably would, as edge cases surfaced — would mean shipping an app update and waiting for every staff member to install it. Worse, a bug in the client filtering would silently corrupt data at the source, before the server ever saw the truth, making it unrecoverable.
By keeping the raw points as the client's responsibility and the interpretation as the server's, I get a clean separation: the client captures faithfully, the server decides authoritatively. The raw data is preserved, so if I improve the filtering logic, I can reason about it against real historical points. The rules are consistent across every device instantly, because there's one place they live. And the trust boundary is exactly where it should be — on the server I control, not spread across a fleet of phones I don't. This is the same instinct that says never trust the client in security: the client reports, the server decides. Here it applies to data integrity rather than auth, but the principle is identical.
The trip lifecycle
Under the hood, a trip moves through a clear lifecycle, and modeling that lifecycle explicitly was one of the early decisions that kept the whole system coherent. A staff member starts a trip on their phone; the app begins capturing GPS points and buffering them. The trip is active — points accumulate, syncing to the server when the network allows. When the person arrives and begins working, the on-site timer starts, marking the transition from travel to work. When they finish, the trip ends, the timer stops, and the trip becomes a complete record: a filtered travel distance, an on-site duration, and the rate snapshot that was captured for it.
Modeling this as explicit states rather than a loose pile of location rows matters because each state has different rules. During travel, we care about distance and we're filtering noise. During on-site work, we care about duration and the GPS is largely irrelevant. The transitions are the meaningful events — start travel, arrive, start work, finish — and building the system around those events rather than around a raw stream of coordinates made everything downstream simpler to reason about. A trip isn't a bag of points; it's a small story with a beginning, a middle, and an end, and each chapter has its own logic.
The on-site timer and staying awake
A small but genuinely tricky piece: the on-site work timer needs to keep running accurately while someone is working, and phones are aggressive about sleeping to save battery. A naive timer that pauses when the screen locks would silently undercount work time — and undercounting work time means underbilling and underpaying, the same integrity failure as phantom distance, just in the other direction.
The answer involves a screen wake-lock to keep the timing reliable while a trip is active, plus designing the timing so it's robust to the app being backgrounded or the device behaving unpredictably, as devices do. The broader lesson is that mobile is a hostile environment for anything that needs to run continuously — the OS is actively trying to suspend you to save power, and you have to work with that reality rather than assume your code just keeps running. Treating the platform's power management as an adversary you design around, rather than a detail you can ignore, is the difference between a timer that's right and a timer that's right only when someone is staring at the screen.
Snapshotted rates: keeping history honest
Here's a subtler design problem that I'm quietly proud of solving, because it's the kind of thing that's invisible when it's right and catastrophic when it's wrong.
The business sets rates: so many rupees per kilometre for travel, so many per hour for on-site work, and these can differ per staff member. Now the obvious way to build this is to store each staff member's current rate and multiply it by their distance and hours when generating a payout. Simple — and quietly broken. Because rates change. When the business raises the per-kilometre rate next month, the obvious design would suddenly recalculate every past trip at the new rate, silently rewriting history and producing payout reports that no longer match what was actually agreed and paid at the time. That's not a rounding error; that's a system you can't trust to tell you what happened.
The fix is rate snapshotting. When a trip is recorded, the rates in effect at that moment are captured onto the trip itself. The trip doesn't store a reference to "the staff member's current rate"; it stores "the rate that applied to this trip, as it was when this trip happened." Change the rates going forward, and every future trip uses the new rates while every past trip remains exactly as it was calculated. History becomes immutable. A payout report from three months ago says the same thing today as it did then, because the numbers it's built from were frozen at the time.
This is a principle that generalizes far beyond this app, and learning it here made me a better engineer everywhere: when you calculate something from values that can change, snapshot the values at calculation time if the result needs to stay stable. Invoices, receipts, payroll, orders — anything financial and historical needs this. The alternative, where changing a "current" value silently mutates the past, is a whole category of data-integrity bug that's insidious precisely because everything looks fine until someone compares an old report to a new one and finds they disagree.
Surviving the real world: offline resilience
A GPS tracker that only works with a perfect network connection is a GPS tracker that doesn't work, because field staff drive through exactly the places networks are worst — rural roads, dead zones, tunnels, the gaps between towers. If the app dropped location data every time connectivity blinked, the distance totals would be full of holes, and holes in the data mean holes in people's pay. Unacceptable.
So OnSite Tracker buffers points locally on the device and survives network dropouts. As a trip is recorded, points are captured and held on the phone; when connectivity is available they sync to the server, and when it isn't they wait, patiently, until it returns. A technician can drive through a twenty-minute dead zone and lose nothing — every point is captured locally and delivered when the network comes back. The trip is complete and accurate even though the connection wasn't.
This is one of those requirements that's invisible in a demo — everything works fine on office wifi — and absolutely essential in production, where the actual users are moving through the actual patchy network coverage of the actual world. Designing for the messy reality of mobile connectivity rather than the clean fiction of a constant connection is a lesson I now carry into everything with a mobile component. The demo lies; the field tells the truth, and you build for the field.
The admin side: monitoring, correction, and reports
The staff-facing app is only half the product. The other half is the admin experience, and it's where the business actually gets its value, so it got just as much care.
Admins get a live dashboard — who is traveling right now, who is on site, updating in real time. There's something genuinely useful about a business being able to glance at a screen and see the live state of its field operations, and building that real-time view was one of the more satisfying pieces of the frontend work.
Admins can also approve or correct trips. This one matters more than it might seem, because it's an admission that even good filtering isn't perfect. Sometimes the GPS was genuinely bad, sometimes something odd happened, and the admin — the human with context the algorithm lacks — needs the ability to review and adjust. Designing the system to expect human oversight rather than pretend to flawless automation is, I think, the honest and correct choice for anything that touches people's pay. The algorithm does the heavy lifting; the human keeps final authority over the money. That's the right division of labor, and it's the same human-in-the-loop philosophy I bring to everything, including how I work with AI.
Finally, admins export date-range payout reports to CSV — the actual deliverable that closes the loop, turning all the captured trips and snapshotted rates into the document the business uses to actually pay people. Every piece of engineering upstream — the filtering, the snapshotting, the offline resilience — exists to make this one report trustworthy. A product like this is ultimately judged by whether the number at the bottom of that CSV is one the business can pay against without arguments, and getting that number right is the whole job.
How do you test something that depends on GPS?
Testing was a puzzle worth describing, because it's a problem you don't hit until you build something like this. You can't drive around for hours every time you change a line of the filtering logic — that's absurdly slow and not repeatable. And the whole point of the filtering is to handle messy, real-world GPS, which is exactly the thing that's hard to reproduce on demand at a desk.
The approach that worked was to treat the filtering as a pure function of a sequence of points and test it against synthetic sequences that reproduce the real failure modes. A cloud of jittery points at a fixed location — does the filter correctly report near-zero distance instead of phantom kilometres? A clean straight-line drive — does it report the right distance? A sequence with a single wild outlier spliced in — does it reject the impossible jump and keep the rest? By constructing point sequences that embody each specific noise pattern, I could verify the filtering behaves correctly against every case that matters, deterministically, in milliseconds, without leaving my chair.
This is the same lesson I keep relearning across projects: make the core logic a pure, testable function of its inputs, and the hardest part of the system becomes the easiest to verify. The filtering doesn't need a phone, a network, or a real drive to be tested — it needs a sequence of points and an expected result. Isolating it from all the messy I/O around it is what made it trustworthy, and trustworthy is the entire product. The moment your money-critical logic can only be tested by physically reproducing reality, you've lost; the moment it's a pure function, you've won.
Real-time, without over-engineering it
The live admin dashboard — seeing who's traveling and who's on site right now — needed to feel genuinely live, but I was careful not to reach for heavier real-time infrastructure than the problem warranted. It's tempting, on a solo project, to either under-build (a dashboard that's stale until you refresh) or over-build (a full real-time event system for what is, honestly, a modest number of field staff).
The right answer was matched to the actual scale: keep the dashboard current enough that it's genuinely useful for an operations person glancing at it, without constructing infrastructure that would only justify itself at a scale this product isn't at. Choosing the proportionate solution — not the most impressive one, the right-sized one — is a discipline solo work teaches hard, because you personally pay the maintenance cost of every bit of complexity you add. On a team it's easy to over-engineer because someone else maintains it later. Alone, you feel every unnecessary abstraction as a weight you'll carry, so you learn to build exactly what the problem needs and not a layer more.
The stack, and owning every layer
I built this solo, end to end, which meant every layer was mine to choose and mine to get right. The frontend and API are Next.js and React with TypeScript. The data layer is Turso (libSQL) in production, with plain SQLite as a fallback for local development — a lightweight, edge-friendly database that fits a product like this far better than dragging in heavy infrastructure would. Styling is Tailwind, deployment is Vercel, and the location capture leans on the browser's Geolocation API on the staff devices.
But the stack is genuinely the least interesting part of the story, and I'd be doing the
project a disservice to dwell on it. The interesting engineering wasn't in choosing tools;
it was in the domain problems — the noise filtering, the rate snapshotting, the offline
buffering, the real-time monitoring — that don't come from any framework. You can't npm install a solution to phantom GPS kilometres. That has to be designed, from an
understanding of the actual problem, and that design is where a product like this is won or
lost.
Owning every layer taught me something no single-layer role does: how the decisions in one layer ripple through the others. The choice to filter server-side shapes what the client must capture. The choice to snapshot rates shapes the data model. The choice to buffer offline shapes the sync protocol between client and server. When you own all of it, you feel these connections directly, and you make decisions with the whole system in view rather than optimizing one layer at the expense of another. That whole-system perspective is, more than any specific technology, what I took away from building this alone.
Designing for disputes, not just happy paths
One design lens that shaped a surprising amount of the product: I built it assuming disputes would happen, rather than pretending they wouldn't. Money changes hands based on these numbers, and wherever money is involved, someone will eventually question a figure — a staff member who thinks their distance is low, an admin who thinks a trip looks wrong. A system that can't answer "why is this number what it is?" turns every such moment into a fight.
So the product is built to be explicable. A trip isn't an opaque total; it's a filtered record you can trace back — this is the distance, here's the on-site time, here's the rate that applied and when it was captured, and the admin can review and correct where the raw data genuinely misfired. That traceability means a disagreement becomes a conversation about a specific, inspectable record rather than a standoff between two people's memories. The immutable rate snapshots matter enormously here too: when a past payout is questioned, the system can show exactly what was calculated and on what basis, and it will say the same thing today that it said then.
Designing for the dispute rather than the demo is a discipline I'd recommend for anything that touches money or trust. The happy path is easy and it's where most software stops. The value is in the awkward moments — the correction, the disagreement, the audit — and building for those from the start is what makes a system people actually trust with their livelihoods rather than one they tolerate until the first argument it can't settle.
The data model, and why Turso
Underneath everything is the data model, and I drew it before writing much code because, as with any system, the schema is the decision everything else inherits. The core entities are staff members, trips, the location points that make up a trip, the rate configuration, and the payout records. The relationships are natural — a staff member has many trips, a trip has many points and one rate snapshot, a payout report spans a date range of trips — and the snapshotting I described earlier is baked right into the shape: a trip carries its own rate values, not a pointer to a mutable rate elsewhere. The data model is where the "keep history honest" principle stops being a good intention and becomes an enforced structure.
For the database itself I chose Turso — libSQL, an SQLite derivative built for the edge — in production, with plain SQLite locally. That choice reflects a philosophy I hold about infrastructure: use the lightest thing that genuinely does the job. A product like this doesn't need a heavyweight database cluster; it needs reliable, fast, well-understood relational storage that doesn't become an operational burden for a solo maintainer. SQLite's model is rock-solid and battle-tested, and Turso extends it to production and the edge without dragging in the operational weight of something far larger. Being able to develop against the exact same database engine I run in production — no "works locally, breaks in prod because the databases differ" surprises — is worth a great deal, especially when you're the only person who'll be debugging it at 11pm. The best infrastructure choice is often the boring one that you never have to think about again.
Tracking people is a responsibility, not just a feature
I can't write honestly about this product without addressing the obvious tension: it tracks people's location. That's sensitive, and treating it casually would be a real ethical failure, so I didn't.
The design is deliberately scoped to the legitimate purpose and no further. The tracking exists to fairly compensate travel and accurately bill work — it captures trips staff explicitly start, for work, not a continuous surveillance feed of someone's entire life. A staff member starts a trip when they're working and the capture is tied to that work activity; it isn't a background process silently following them everywhere, always. That scoping is an ethical choice expressed in the architecture: build the system to serve its stated purpose and to be structurally incapable of casually overreaching beyond it.
I think about this the way I think about all data: collect what you genuinely need for the purpose the user understands, and nothing more. A location-tracking product carries an obligation to be scrupulous about scope, transparent about what it does, and designed so that the tracking serves the people involved — fair pay for staff, accurate billing for the business — rather than becoming surveillance for its own sake. The fact that a capability could be abused is exactly why the responsibility to design it well sits with the engineer. Getting the ethics right isn't separate from getting the engineering right; on a product like this, it's part of the same job.
What building it solo taught me
There's a particular clarity to shipping a real product entirely on your own. There's no one to hand the hard part to, no layer that's "someone else's problem," no ambiguity about who owns the bug. That's daunting, and it's also the fastest way I know to grow, because you cannot hide from any part of the craft. You will confront the messy data problem, the data-modeling problem, the real-time problem, the deployment problem, the "it works on my machine but not in the field" problem — all of them, because they're all yours.
It also forced a kind of ruthless prioritization. Solo, with limited time, you cannot gold- plate everything, so you learn to spend your effort where it actually matters: the filtering that makes the numbers trustworthy, the snapshotting that keeps history honest, the offline resilience that makes it work in the field. Those got deep care because they're the product. Other things got the minimum that was genuinely sufficient. Learning to tell the difference — where to invest and where "good enough" is genuinely good enough — is one of the most valuable skills solo work teaches, and it's directly transferable to how I prioritize on a team.
And it reinforced the belief that runs through everything I build: software is at its best when it takes something messy and human — trust, honesty, fair pay — and makes it reliable. OnSite Tracker doesn't just track GPS. It replaces a system of guesswork and disputes with one of trustworthy numbers, so that a business pays its people fairly without either side having to rely on the other's memory or good faith. That's a small thing and a real thing, and it's live, doing that job, right now. Building something people actually depend on, alone, and having it hold up in the real world — that's the kind of engineering that made me sure this is the work I want to do.
The thread to everything else I build
Looking at OnSite Tracker next to my other work, the connective thread is obvious once you see it. The AI home-design platform turned a family's fuzzy needs into concrete designs and honest costs. My structural-health-monitoring research was about turning a bridge's raw vibrations into a trustworthy warning. The Sipo platforms turn a restaurant's operational chaos into a running system. And OnSite Tracker turns noisy GPS into fair pay. Different domains, wildly different technologies, but the same fundamental act every time: take a messy, real-world signal and make it into a trustworthy number or decision a human can act on.
That's the kind of engineering I care about most, and building OnSite Tracker alone clarified it for me more than anything else. It's not about the flashiest technology or the trendiest stack. It's about understanding a real problem deeply enough to build software that people can actually rely on — where the number at the bottom of the report is one they'll pay against without a second thought, because the engineering behind it earned that trust. Every filtering rule, every snapshotted rate, every buffered point in a dead zone is in service of that trust. Earn it, and you've built something that matters. Miss it, and you've built something that's confidently wrong, which is worse than useless.
So this is the honest version of my "workflow": it's not a stack, and it was never really front-end. It's a way of approaching problems — understand the mess, find where the raw signal lies, design the thing that makes it honest, keep a human in the loop for the calls that matter, and hold the whole system to the standard of would I trust the number this produces? That approach doesn't change whether I'm writing a React component, a GPS filter, a FastAPI endpoint, or a Flutter screen. The tools are incidental. The judgment is the job.
If you're building products that turn noisy real-world signals into trustworthy numbers — GPS, sensors, anything where the raw data lies and the stakes are real — I'd love to compare notes. Reach out.