Building Photo Proof of Delivery: Camera to Dashboard, End to End

TIL that the hard part of a photo-proof-of-delivery feature isn't the camera or the upload individually — it's making sure a completed delivery can never end up without its proof attached.

3 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

"Take a photo when you complete a delivery" sounds like a small feature — camera, upload, done — until you consider what happens when any single step in that chain fails and the driver has already moved on to the next stop.

Context #

The full path is: driver opens camera → captures photo → app uploads it → API stores it and links it to the delivery record → dispatcher's web dashboard displays it as proof. Five steps, and the business only cares that the last one eventually succeeds.

What I Tried #

Built it as a straight sequence: capture, then upload, then mark the delivery complete, each step blocking the next — treating "delivery complete" as something that only happens after a successful upload.

What Went Wrong #

On a weak connection (common for drivers moving between areas), the upload step could take long enough that the driver assumed the app had frozen and backed out or moved to the next order, leaving the delivery neither marked complete nor retried.

The Solution #

Decoupled "delivery marked complete" from "photo successfully uploaded." The app marks the delivery complete locally and optimistically the moment the photo is captured, queues the upload in the background with retry, and only reconciles with the server once the upload actually succeeds — matching the same local-first pattern used elsewhere for order status updates.

async function completeDelivery(order, photoUri) {
  await markCompleteLocally(order.id); // instant, driver can move on
  await queuePhotoUpload({ orderId: order.id, photoUri }); // retried in background
}

Why It Works #

The driver's actual job — deliver the package, capture proof, move on — doesn't need to wait on network conditions outside their control. Separating "the delivery happened" (a fact about the physical world, true the moment the driver acts) from "the proof has synced" (a fact about network reliability) means neither blocks the other, and the background queue handles reconciliation without the driver having to babysit an upload.

Lessons Learned #

A feature that touches camera, storage, network, and a backend record isn't one feature with five steps — it's five failure domains, and the one that matters most (does the business ever lose proof of a real delivery) has to be designed for explicitly, not assumed to follow from each step individually working.

What I Would Do Differently #

I'd design the "what happens if step 3 fails after step 2 succeeded" question before writing the happy path, instead of building the sequence first and retrofitting resilience after a driver reported a lost delivery.

Local-first mutation patterns, background upload queues, decoupling business state from sync state.