← Back to case studies

Case Study

Building a Reliable Field Survey App for Low-Connectivity Environments

A mobile-first PWA for survey engineers capturing building assessments in low-connectivity environments, designed to preserve answers and hundreds of photos without slowing field work.

Project Overview

This project was a mobile-first Progressive Web App for a building certification platform. Survey engineers used it on phones and desktops to assess large commercial buildings, answer structured survey questions, and capture photographic evidence while moving through the property.

The Challenge

The core requirement was simple to state but difficult to execute: survey engineers needed to keep working even when the building had poor or no internet connectivity. A single survey could involve a large building with 50 floors, multiple basements, elevators, and service areas where connectivity changed constantly.

The app could not slow the engineer down while they were answering questions and capturing evidence. At the same time, it could not risk losing survey answers or photos. If an engineer captured 600 photos during a visit, every image needed to be accounted for. Some images might reach S3 immediately, some might be stuck on the device for hours, and the survey still had to make sense at the end.

A single survey session could last 6 hours and involve 500-600 images. Memory management became a product requirement, not just a technical nice-to-have. The phone might also need to hold multiple surveys at the same time, so local storage had to be visible, bounded, and recoverable.

My Process

I approached the problem as a local-first workflow rather than an upload-first workflow. The user action had to complete locally first: capture the photo, show the thumbnail, update the survey state, and preserve enough data that the network could fail without breaking the survey.

From there, I separated the heavier work into a background pipeline: image compression, IndexedDB persistence, signed URL requests, thumbnail upload, and full photo upload. In other words, the app behaved less like a form and more like a tiny field-data operating system running inside the browser.

Diagram showing offline field survey capture with IndexedDB, web workers, periodic sync, and S3 upload

Solution

We designed the app around a local-first capture pipeline. The PWA accepted survey answers and photos immediately, saved critical data locally, and treated network upload as a background process that could succeed now, later, or during final submission.

async function capturePhoto(file: File) {
  // UI feedback first. Network can catch up later.
  const thumbnail = await createThumbnail(file);
  showThumbnail(thumbnail);

  // Persist evidence before attempting upload.
  await indexedDb.photos.put(file);
  await indexedDb.thumbnails.put(thumbnail);

  taskManager.enqueue(file);
}

When an engineer selected or captured photos, the app created upload tasks and immediately generated a local thumbnail. That thumbnail was saved and displayed in the survey UI, so the engineer had instant feedback that the photo was captured successfully. That tiny thumbnail mattered: it told the engineer, "you got the evidence, keep moving."

Behind the scenes, the task manager picked up the work. It calculated a safe number of workers from device capabilities, sent each image task to a worker, and assigned the next pending task when a worker finished. While the user continued the survey, workers compressed photos, saved them to IndexedDB, requested signed URLs, and uploaded both thumbnails and full images to S3.

const TWO_MINUTES = 2 * 60 * 1000;

function startSurveySyncJob(surveyId: string) {
  setInterval(async () => {
    await syncSurveyJson(surveyId);

    const pending = await findUnsyncedPhotos(surveyId);

    // Wake workers only when there is useful work.
    taskManager.wakeSleepingWorkers(pending);
  }, TWO_MINUTES);
}

Storage and Recovery

We also added a storage screen because offline systems need visibility. This became a critical fallback: survey engineers could forget how much evidence the app was holding after a long day in the field, and the storage screen gave them a clear picture of what was still on the device.

This page gave users and support teams a recovery path. If something unusual happened, such as battery loss or browser memory pressure, the user could return to the app, inspect what was still local, manually sync the survey, and clear storage only after the data had safely reached the server.

Results and Impact

The result was a field survey app that matched the reality of large buildings. Engineers could keep answering questions and collecting evidence without waiting for the network, while the system continuously worked in the background to compress, persist, upload, and reconcile survey data.

async function submitSurvey(surveyId: string) {
  const pending = await findUnsyncedPhotos(surveyId);

  // Resumable: only upload missing evidence.
  const result = await uploadMissingAssets(pending);

  if (result.failed.length) {
    throw new Error("Some photos are still pending. Please retry sync.");
  }

  await submitSurveyJson(surveyId);
  await clearLocalSurveyStorage(surveyId);
}

Reflections

The biggest lesson from this project was that offline support is not a single feature. For this workflow, it affected capture UX, storage limits, background processing, upload retries, final submission, and support tooling. The app had to make progress locally first, then reconcile with the server whenever the network allowed it.

If I were extending this further, I would invest more in visual observability for the sync pipeline: clearer per-image status, better queue-level progress, and a diagram that helps users and support teams understand exactly where a photo is in the flow.