NFC Based Employee Time Tracking
An attendance system where the shift clock can't be gamed by changing the phone's time
What it is
A company needed staff to clock in and out across several facilities. Three NFC tags — start shift, start break, end shift. Each scan posts to the backend.
I was the only developer on it. Every structural decision was mine: folder layout, libraries, state, how the app talked to the server. It's live on the App Store and around twenty people use it daily for their shifts. It's an internal tool, not a consumer app.
The problem
The client came back after release: staff were awarding themselves hours.
The app was reading the phone's clock. The phone's clock belongs to the user. Change the system time before scanning out and you get paid for hours you didn't work — and the backend had no way to tell, because it was being handed a timestamp with nothing to check it against.
What I proposed first, and why it was rejected
My first answer was to detect the manipulation: notice the clock had moved, block the app, flag the user.
The client rejected it outright. It punished employees for something the app had got wrong, and it turned a payroll tool into a surveillance one. They were right, and it's the more useful half of this story — the fix that ships is rarely the first one you're pleased with.
What actually worked
Make the server the source of truth, and stop the in-app timer depending on the system clock at all.
At login the app stores two things together: the server's timestamp, and the device's time since boot at that same instant. From then on, the time it displays is that anchor plus however far time-since-boot has advanced.
Time since boot isn't user-editable. You can set the system clock to any year you like and the running timer doesn't move.
The clock that can't be gamed
Two timers for the same shift. One trusts the phone's clock; the other is anchored to server time plus time-since-boot. Change the clock and watch them disagree.
A shift in progress. Both timers agree — for now. Try winding the phone's clock.
There's no cross-platform API for this, so I wrote a native module on each side —
SystemClock.elapsedRealtime() on Android, mach_absolute_time on iOS — and
bridged it to the JavaScript layer. That part had no useful AI output; it meant
reading both platforms' docs and writing the bridge by hand.
A reboot resets time-since-boot, which is exactly why the app re-fetches from the server when it relaunches. The backend returns the current time and the last mode change, and the app rebuilds the shift or break in progress from those two facts.
Offline
Some facilities had no reliable network. Scans queue on the device and sync when a connection returns; the next day's shift requires a sync before it starts, so the queue can't grow indefinitely.
What's still open
Offline and rebooted, together. The anchor is gone and there's no network to re-fetch it. I haven't solved that case, and I'd rather say so than pretend the design is airtight.