Back
12 min read

Building a Construction Inspection App for US State Government: What I Learned

React NativeGovernmentConstructionOffline-FirstFirebase

The Context

Not every app you build is a consumer product.

The construction inspection app I worked on is used by US state government inspectors who walk active construction sites — bridges, highways, public buildings — and document compliance.

These inspectors don't care about animations, dark mode, or onboarding flows. They care about:

  • Does the app work when there's no signal?
  • Can I submit an inspection in under 5 minutes?
  • Will I lose data if the app crashes?
  • Is it legally defensible in court?

That last one changes everything.


The Architecture

React Native App (offline-first, multi-tenant) | WatermelonDB (Encrypted via SQLCipher) | Sync Engine (queue-based, mutex-locked) | REST API (Node.js/Express) | Cloud Database (PostgreSQL)

What I Learned

1. Offline-First in extreme environments

Inspectors work in remote highway segments, concrete tunnels, and under massive steel bridges where cellular signal is non-existent. We had to build a true offline-first architecture.

All inspection data, checklists, and photo attachments are stored locally using WatermelonDB. To sync with our backend database, we used WatermelonDB's built-in synchronize() function, which communicates with our REST API.

The sync cycle looks like this:

  1. Pull Changes: The app calls a GET endpoint (/api/sync?lastPulledAt=...) to fetch all server-side database changes since the last sync.
  2. Consume Changes: WatermelonDB consumes the JSON response and applies the creates, updates, and deletes to the local SQLite database in a single batch.
  3. Push Changes: The app calls a POST endpoint (/api/sync) with all locally queued changes. The backend resolves conflicts and updates the primary PostgreSQL database.

This REST API-driven sync cycle handles intermittent connectivity flawlessly.

Handling Large Attachments Offline

Construction inspections require extensive photographic proof. Inspectors capture high-resolution photos that could easily exceed 10MB each. Attempting to sync these over a fragile 3G connection would crash the app.

We implemented an offline media pipeline:

  1. On-Device Compression: Upon capture, images are compressed using an native image resizer, dropping the resolution to a maximum of 1920x1080 and setting JPEG compression to 80%. This reduced file sizes from 10MB to ~300KB without losing compliance-grade detail.
  2. Local Storage: The compressed images are saved directly to the device's document directory, and only the local file URI is stored in WatermelonDB.
  3. Background Sync: Instead of uploading photos inline with form data, we use a separate background file transfer daemon. Form sync completes in seconds, and photos are queued and uploaded asynchronously in the background.

2. Accessibility audits (Section 508) and local security

In government software, Section 508 compliance is a legal requirement. We had to meet WCAG 2.1 AA standards. This meant auditing every component for screen reader compatibility, ensuring minimum contrast ratios, and designing touch targets large enough to be operated by inspectors wearing heavy-duty work gloves on-site.

Here is an example of an accessible form input field we built:

tsx
import React from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';

interface AccessibleInputProps {
  label: string;
  value: string;
  onChangeText: (text: string) => void;
  error?: string;
  placeholder?: string;
}

export const AccessibleInput: React.FC<AccessibleInputProps> = ({
  label,
  value,
  onChangeText,
  error,
  placeholder,
}) => {
  return (
    <View style={styles.container}>
      <Text style={styles.label}>{label}</Text>
      <TextInput
        value={value}
        onChangeText={onChangeText}
        placeholder={placeholder}
        style={[styles.input, error ? styles.inputError : null]}
        accessible={true}
        accessibilityLabel={`${label}`}
        accessibilityHint={error ? `Error: ${error}` : `Enter your ${label}`}
        accessibilityRole="keyboardkey"
      />
      {error && (
        <Text
          style={styles.errorText}
          accessible={true}
          accessibilityRole="alert"
        >
          ${error}
        </Text>
      )}
    </View>
  );
};

Local Database Encryption

Because the app captures sensitive structural infrastructure data, we couldn't store it in plain text SQLite. We encrypted the local SQLite database using SQLCipher.

Key secrets are generated using cryptographically secure random bytes on the first launch, stored in the native iOS Keychain and Android Keystore, and used to decrypt the database on app initialization. We also implemented SSL pinning to prevent man-in-the-middle attacks on the sync engine.


3. Navigating government bureaucracy and approvals

In Gov-Tech, coding is only half the battle. Project velocity is heavily dependent on non-technical workflows:

  • Security Audits: Passing agency-specific penetration testing, static analysis checks, and data-governance reviews.
  • Workflow Approvals: Aligning different state departments of transportation (DOTs) who had completely different regulatory requirements for the same inspections.
  • Stakeholder Training: Ensuring simple, documented UI flows because inspectors range from tech-savvy newcomers to veterans who prefer paper forms.

The Hardest Bug: The Sync Race Condition

An inspector in the field reported that occasionally, inspections would appear duplicated after sync.

After a week of investigation, the root cause was a race condition in the sync queue. When the app came back online, it would trigger multiple sync attempts simultaneously if multiple changes had accumulated. The retry logic would re-queue items that were already being processed.

Fix: a simple mutex lock on the sync process:

tsx
let isSyncing = false;

async function processSyncQueue() {
  if (isSyncing) return;
  isSyncing = true;
  try {
    const queue = await getQueue();
    for (const item of queue) {
      await syncItem(item);
      await removeFromQueue(item.id);
    }
  } finally {
    isSyncing = false;
  }
}

Obvious in hindsight. Cost a week of debugging.


What I'd Do Differently

More testing on low-end devices. Government budgets don't buy new tablets every year. Some inspectors were using 5-year-old Android devices. I should have tested on these earlier in the cycle.

Better error reporting. The first version had basic error logging. I needed full crash reporting with device state (network, battery, storage) to debug field issues. Crashlytics was a late addition that should have been there from day one.

Involve inspectors in UI design. The form layout made sense to me. It didn't make sense to someone filling it out while wearing gloves on a construction site. Button sizes, contrast ratios, and tap targets all needed adjustment after real-world testing.


The Bottom Line

The app runs reliably in production across multiple states. Inspectors complete hundreds of inspections monthly. The sync engine handles intermittent connectivity without data loss. The accessibility compliance passed audit.

Government software has a reputation for being bad. This one isn't.