Before this starts: this was a hackathon, an afternoon spent proving an idea was technically possible, not a production rollout. The pending-check record this design depends on already existed in production, it backs an existing manual review process, so I built the Static Web App, the Function, and the stored procedure against the real table rather than a mock, and a few of us tested actual taps through to a completed record. It worked. It was not rolled out past that testing, we stopped short of a full rollout because the hardware to support it is not in place yet.

Where the idea came from

Another Accidental Hackathon

Before any of the design work, there was a much less structured conversation with my boss about how to actually stop drivers skipping the rear check. We tried a few ideas on paper first, a checklist screen, a confirmation button, a photo requirement, and every one of them had the same hole: a driver at the end of a long shift can tick a box or tap a button without actually walking anywhere. What we agreed on was that the control had to force a physical action, not just capture a digital one. It would not stop someone determined to bypass it, but it raises the effort compared with a checkbox.

NFC as the mechanism was not something I researched from scratch. I already knew cheap writable NFC tags existed and could open a URL on tap, from watching a mate write one with his phone years ago for something as mundane as sharing his home wifi password. That was enough of a mental model to reach for when my boss and I landed on "force a physical action." Nobody scoped this as a formal project before it started. It just needed an afternoon of checking whether the idea held up once I put it in front of a proper design review.

The problem

A Safety Check That Has to Be Almost Invisible

The requirement itself was straightforward. When a driver ends a duty, they need to physically walk to the rear of the vehicle and check the passenger area before the duty can close. It is a procedural control, not a clever piece of engineering: the goal is to change driver behaviour, not to build a surveillance system. The idea was to put an NFC sticker at the rear of the vehicle. Tap it, duty can end. Do not tap it, duty stays open.

The constraint that shaped everything else was the driver population. Some of our drivers are older and have limited patience for technology. Asking them to log into a second app, remember a PIN, or troubleshoot a broken session at the back of a vehicle in the rain was never going to work. Whatever this became, it had to work with a tap and nothing else. No login, no app install, no second thing to remember.

We already have an authenticated driver app that drivers use for other parts of the duty, with a shortcut icon on their device that opens straight to a web page they log into. So routing this check through that existing session was on the table from the start, not something I discovered later. I chose not to, for one practical reason: a driver sometimes needs to tap this with their own phone if the tablet is flat, lost, or having a bad day, and they have never logged into the driver app on their personal phone. Forcing a login at that point defeats the whole reason this needed to be tap-only. That fallback case mattered more to me than tighter authentication, so I designed the check to work without relying on any existing session at all.

That single constraint, no authenticated driver session at the point of the scan, is what made this an interesting design problem rather than a routine one. Everything downstream had to be built around the fact that the thing tapping the tag could not prove who it was.

First pass, and a wrong assumption

Designing Around Authentication I Did Not Have

My first pass with Claude produced a genuinely reasonable design: a short-lived challenge created when the driver presses End Duty, an authenticated POST to confirm the check, server-side validation of driver, duty, vehicle and expected tag. Textbook, defensible, and completely unusable for us. I had to stop and point out that there is no driver authentication at the tap point. Older drivers struggling with a simple app was the whole reason this needed to be tap-only in the first place. The response had quietly assumed a login flow into existence because that is the "correct" way to secure a write endpoint, without checking whether that assumption matched my actual constraint.

That correction turned into something more useful: a proper explanation of what an ordinary writable NFC tag physically can and cannot do. It cannot hold a clock. It cannot execute code. It cannot perform an HTTP POST on its own, because it just stores a static NDEF record, normally a URL, that the phone's operating system opens as a navigation request. Any timestamp has to come from the server that receives the tap, not from the tag. That single fact ruled out a whole category of designs where the tag itself carries a signed, time-stamped payload. A cheap static sticker cannot do that. If I wanted that level of assurance later, I would need something like an NTAG 424 DNA tag with a real chip on it, not the ten-cent stickers I am using for this proof of concept.

๐Ÿ’ก

What a static NFC tag actually proves: almost nothing on its own. Because the URL can be copied and reopened later, it does not even prove that specific sticker was tapped just now, only that a request tied to that tag's token arrived at some point. It does not prove anyone was standing at the vehicle when it did, and it does not stop someone bookmarking the URL and opening it later from the driver's seat. For a proof of concept whose purpose is behavioural rather than cryptographic, I decided that limitation was acceptable, provided it was written down rather than quietly assumed away.

Once the authentication assumption was gone, the actual shape of the problem became clearer. The system does not need to know who scanned the tag. It only needs to know that a scan for a specific vehicle happened inside the window where that vehicle currently had an open end-of-duty request. Identity and proof of scan turned out to be two separate concerns that had been tangled together in the first draft.

Landing on a shape

Separating "A Scan Happened" From "The Duty Is Complete"

The architecture that fell out of that reframing is fully serverless, which fits how the rest of our stack is built. A driver presses End Duty on the existing authenticated tablet app. That creates a pending check record: this vehicle, this duty, requested at this time, expires in a few minutes. The driver walks to the rear and taps the tag. The tag opens a public Static Web App, unauthenticated by design, which calls a single Azure Function. At the database level, the Function's managed identity has execute permission on exactly one stored procedure, with no direct table access, no db_datawriter role, and no credentials sitting in a connection string.

// request path
Driver presses End DutyAuthenticated tablet app creates a pending check, time-bound to this vehicle and duty
Driver taps the rear NFC tagStatic tag, no clock, no code, just a random opaque URL
Public Static Web App loadsUnauthenticated, shows "Recording check..." before anything fires
Azure Function, managed identityExecute-only on one stored procedure at the database level, no table access
Stored procedure resolves, records, matchesServer timestamp, atomic match against the open pending check, single transaction

The tag carries no personal or operational data, just a long opaque token that maps to a vehicle. No driver ID, no route information, no passenger details, nothing that would matter for privacy if someone photographed the sticker. It still has to be treated as a bearer capability though, anyone who obtains the token, not just the person standing at the vehicle, can generate a request that looks exactly like a genuine tap, which is the whole basis of the replay discussion later in this post. The server timestamp, not anything the tag or phone provides, is what gets recorded. Matching a scan to a pending duty happens entirely server-side, inside the stored procedure, never in the tablet's JavaScript and never in the anonymous Function itself.

The stored procedure also writes every scan to a separate log table, not just the one that ends up matched to a duty, an application-level append-only log, meaning the code path only ever inserts into it, not a database-enforced immutable store. A tap that arrives with no open pending check still gets recorded, it just does not complete anything. Each row carries the server timestamp, the tag identifier, and whether it matched an open duty or not.

That log is the reason the bearer-capability problem above is manageable rather than alarming, though it is worth being precise about what it actually gives me. It is not active monitoring, nothing watches this table or raises an alert on its own. What it gives me is the ability to tell, after the fact, whether a token has been used more than expected, which is enough to investigate a suspected leak and decide whether that tag needs switching to the authenticated route. It would also help with the more mundane version of that question, a driver disputing whether they tapped the tag, under the manual process this would eventually replace. A row in that table is evidence a request for that token reached the backend at a given time, matched or not. It is not proof a physical tap happened, the post has already been clear that nothing here can prove that, so I would treat it as something to investigate with, not something that settles an argument on its own.

Worth revisiting if tablet-only scanning ever becomes acceptable, rather than treating the current design as final.

A second opinion, and a genuine disagreement

Bringing In a Second Opinion

By this point I had a design I was fairly happy with. Before building it, I did what I usually do for anything with real consequences: I put the whole design in front of a second model, ChatGPT, and asked it to find the holes. Most of the feedback was refinement, a two minute grace window for network delay, keeping the anonymous landing page on its own Static Web App rather than carving an anonymous route into the authenticated one. But one piece of feedback pushed directly against something Claude had specifically recommended.

My design had the NFC tag open a small public page that used JavaScript to fire a POST to the Function, rather than having the tag's GET request write the record directly. ChatGPT's read was that this bought nothing. Both designs are equally replayable, since anyone who copies the URL can trigger either one, so the JavaScript hop was just an extra network round trip and a failure mode for old phones with patchy depot signal.

G
ChatGPT, OpenAI
"The JS-mediated POST is adding complexity without buying you anything."

Both the GET-writes-directly design and the GET-then-JS-POST design are equally vulnerable to someone copying and replaying the URL. Given that, the extra hop is pure overhead: a JS dependency, a second round trip, and a blank-screen failure mode on old phones with weak depot reception.

C
Claude, Anthropic
"The failure mode they're describing is different. It's not deliberate replay, it's unintentional triggering by things that fetch URLs without executing JS."

Correct on replay risk, both designs are equally exposed there. But a driver confused about what a link does might text it to a supervisor, and link-preview generation in WhatsApp or iMessage fetches the URL server-side with no JS execution and no human intent behind it. A GET-writes-directly endpoint would silently record a scan from that alone.

Neither model was wrong about the mechanics. They agreed completely on how replay works and on the fact that a motivated person can trigger either design. Where they diverged was on which threat mattered more to weight for, deliberate bypass versus accidental triggering by an unsophisticated user sharing a link they do not understand. Given the driver population this is built for, I weighted accidental triggering higher and kept the JavaScript layer for that reason. That does not make deliberate bypass a non-issue, someone determined to skip the check can still copy the URL and trigger either design, and that is exactly the kind of assumption a production rollout would need to revisit rather than inherit from a POC.

"The JS gate earns its keep here specifically because of who's using the system, not despite it. Worth being precise about which threat you're actually defending against, since that also tells you the JS gate is a UX safeguard more than a security control."

"Is this secure" is not a precise enough question to design against. "Secure against what, and from whom" is, and that's the question the disagreement actually forced me to answer.

Where the design settled

What Survived the Disagreement

A few things came out of that exchange that I would not have landed on working with a single model.

Kept

The Static Web App landing page, with JavaScript in front of the write

Not for replay protection, both models agreed that does not exist here, but because it filters out the specific accidental-trigger case that matters for this driver population: a confused person sharing a link that then gets server-side previewed by a messaging app.

Adopted

Atomic matching inside one stored procedure

Resolve the token, record the scan, find the open pending check, and mark it complete, all inside a single transaction. Doing the match as a separate step afterwards opens a race window where a duplicate tap and a retry could both see no completed check yet and both try to complete the same duty.

Adopted

Hashing the tag token rather than storing it in plain text

The Function hashes whatever token it receives and compares it against a stored hash. That limits the damage specifically from disclosure of the mapping table itself, a compromised read, a misconfigured report, a stray backup, it does not stop a token leaking some other way, through logs, browser history, or a screenshot of the URL.

Corrected

The grace period is about latency, not clock skew

My first framing called it a clock skew allowance. Since both the pending-check timestamp and the scan timestamp come from server-side UTC, there is no client clock to skew. The grace window exists for network delay and driver pace, and it should be labelled that way in the schema and the documentation.

Two things both models agreed on immediately, and I think both are non-negotiable for anything this close to a compliance record. First, the wording on the confirmation page and in any documentation has to be accurate about what the system actually proves. It proves that a request tied to the vehicle's tag arrived within the required window. It does not prove the driver inspected every seat. "Vehicle check recorded" is honest. Language implying every seat was inspected and confirmed clear is not, and should be avoided everywhere in this system. Second, there needs to be a way to revoke a lost or damaged tag and issue a replacement against the same vehicle without a deployment, because a sticker that gets peeled off or photographed is a predictable operational case to plan for, not an edge case to ignore.

โš ๏ธ

The one limitation with no software fix: a static NFC sticker cannot cryptographically prove physical presence. Someone can copy the URL and open it later without being at the vehicle. For this proof of concept that is an accepted, documented limitation because the control is behavioural, the theory is that the tag's placement inside the rear passenger area is what gets the driver to actually walk back and look, not the software. That is a hypothesis about driver behaviour, not something I have tested with a real depot yet. A production hardening pass would look at dynamic tags like NTAG 424 DNA, which can produce tap-specific cryptographic data. That is not needed to find out whether the basic process works.

Where it actually stands

It Worked. It Is Not Live.

I built the full path, Static Web App, Function, stored procedure, against the pending-check table already in production, and a few of us tapped real tags through to a completed record rather than testing against mocked data. It works. What it is not is a feature every driver can use. That was never the goal of this round. The goal was to find out whether the idea held up once it existed as actual code talking to the real system, and not just a design conversation with two AI models, and it did.

Here are the two screens the test scans actually produced, deliberately plain, deliberately not trying to look more certain than the system is.

On tap
๐Ÿ•

Recording vehicle check...

Please keep this page open.

On success
โœ“

Vehicle check recorded

This scan has been recorded. You can put your phone away now.

That gap between the two screens, the moment the page says "keep this open" before it knows the answer, is the JavaScript hop from earlier in this post actually doing its job. A confused person forwarding the link is very unlikely to generate that first screen at all, since nothing runs without the page actually loading and executing. In my test runs on ordinary connectivity the whole thing completed in under a second, though I have not measured it against real depot signal yet, which is exactly the kind of number I do not want to promise until I have.

What blocks a full rollout is mostly not code. The tags themselves are cheap, but rolling this out properly means the next round of asset purchases needs to account for NFC-capable devices at the depots this would actually run on, and that is a procurement conversation with its own timeline. There is still some code left too, the tag revocation workflow is still a manual SQL update rather than a proper interface, and I have not locked in the exact grace window, somewhere between one and two minutes felt right on paper but I want real depot signal before I trust that number over a guess. None of that blocked proving the concept against the real system. All of it blocks turning it into something every driver can use.

One more thing worth being upfront about: the pending-check record this design writes to is the same one an existing manual review process already uses to satisfy this requirement in production today, the table already had fields for a system check and a manual review before NFC ever came into it. NFC was built and tested to write to that same record, not a parallel one. What actually completes the requirement in production right now is that other, existing method, not NFC, and that method deserves its own explanation rather than a paragraph tacked onto this one. This post is entirely about the design and build process that got the NFC path proven against the real system, not a description of what closes the requirement today.

Where this could go

One Pattern, Possibly More Uses

Once the scan-and-match flow was working, I put the shape of the whole thing back in front of ChatGPT, not to review the vehicle check this time, but to ask whether the pattern, tap a physical object, open a workflow, record what happened, might fit anything else at Ritchies. It came back with a long list: defect reporting, pre-start checks, cleaning and lost-property handling, emergency equipment inspections, a workshop digital vehicle record, and more. None of it is built, tested, or funded. I am not going to lay it out as a roadmap here, that would be getting ahead of a project that had not even passed procurement at that point. The two that stood out enough to mention are defect reporting and pre-start checks, mainly because the tag already identifying the vehicle removes a step drivers currently do manually. Static tags like the ones I have stop being adequate anywhere the business needs proof a person was physically present rather than just identifying an asset. That is a different category of hardware and a different project.

โœ“

What the disagreement actually exposed: Claude and ChatGPT agreed on the mechanics and still landed on different recommendations, because I had not explicitly ranked which threat mattered more for this driver population. That is not a flaw in either model's reasoning. It is a gap in my own threat model that the disagreement forced me to close.

The next post on this will be about that other method, the one that actually closes this requirement in production today, what it is, and why NFC hasn't replaced it yet.