Skip to main content
The PDF Form Parser includes comprehensive offline support, allowing technicians to download inspection data, work offline in the field, and synchronize changes when connectivity is restored.

GET /api/v1/inspections/:id/offline_data

Download complete inspection data for offline use, including all form fills, photos, and metadata.

Authentication

Requires user authentication. Users can only access inspections they have permission to view based on Pundit policies.

Path parameters

id
integer
required
Inspection ID to download

Response

success
boolean
Request success status
data
object
Complete inspection data package
message
string
Success message

Example response

{
  "success": true,
  "data": {
    "inspection": {
      "id": 123,
      "date": "2024-03-20",
      "notes": "Annual inspection",
      "status": "in_progress",
      "system_category": "Fire Sprinkler System",
      "interval_category": "Annual",
      "job": "JOB-2024-0123",
      "created_at": "2024-03-01T09:00:00Z",
      "updated_at": "2024-03-15T14:30:00Z",
      "property": {
        "id": 45,
        "property_name": "Main Office Building",
        "address": "123 Main St",
        "city": "San Francisco",
        "zip_code": "94102"
      },
      "customer": {
        "id": 78,
        "name": "Acme Corporation",
        "email": "[email protected]",
        "phone_1": "555-0100",
        "phone_2": null
      },
      "form_template": {
        "id": 5,
        "name": "Annual Fire Sprinkler Inspection"
      }
    },
    "form_fills": [
      {
        "id": 456,
        "inspection_id": 123,
        "form_template_id": 5,
        "pdf_generation_status": "ready",
        "data": {
          "Building_Name": "Main Office",
          "Location_row_1": "First Floor"
        },
        "created_at": "2024-03-01T09:05:00Z",
        "updated_at": "2024-03-15T14:30:00Z",
        "form_structure": "[{\"name\":\"Building_Name\",\"type\":\"Text\"}]",
        "photos": [
          {
            "id": 789,
            "filename": "inspection_123_photo_1.jpg",
            "content_type": "image/jpeg",
            "byte_size": 245678,
            "url": "/rails/active_storage/blobs/.../inspection_123_photo_1.jpg"
          }
        ]
      }
    ],
    "sync_metadata": {
      "downloaded_at": "2024-03-15T15:00:00Z",
      "version": 1,
      "checksum": "5d41402abc4b2a76b9719d911017c592"
    }
  },
  "message": "Inspection data retrieved successfully"
}

Error responses

error
string
Error identifier:
  • InspectionNotFound - Inspection doesn’t exist or no permission
  • InternalServerError - Server error during data retrieval

Offline-first architecture

The offline support follows an offline-first design where the mobile app:
  1. Downloads complete inspection packages to local storage (IndexedDB)
  2. Works offline using locally cached data and form structures
  3. Tracks changes made while offline
  4. Syncs changes when connectivity returns
  5. Handles conflicts when server data has changed

Data embedding strategy

From inspections_controller.rb:56-57:
# Embed form structure directly in each form_fill
form_structure: form_fill.form_template&.form_structure
Key design decision: Each form_fill includes its own embedded form_structure. This ensures the mobile app can render forms entirely from the cached form_fill object without additional lookups, even when templates are updated on the server.

Storage architecture

Mobile apps should store downloaded data in IndexedDB with this structure:
// IndexedDB stores
const stores = {
  inspections: { keyPath: 'id' },
  form_fills: { keyPath: 'id', indexes: ['inspection_id'] },
  photos: { keyPath: 'id', indexes: ['form_fill_id'] },
  sync_queue: { keyPath: 'local_id', autoIncrement: true }
};

Offline workflow

1

Download inspection

Before going offline, download the inspection using GET /api/v1/inspections/:id/offline_data
const response = await fetch(`/api/v1/inspections/${inspectionId}/offline_data`, {
  headers: { 'Authorization': `Bearer ${token}` }
});
const { data } = await response.json();

// Store in IndexedDB
await db.inspections.put(data.inspection);
await db.form_fills.bulkPut(data.form_fills);
2

Work offline

Use locally stored data for all form operations:
// Read from IndexedDB
const formFill = await db.form_fills.get(formFillId);
const formStructure = JSON.parse(formFill.form_structure);

// Render form using embedded structure
renderForm(formStructure, formFill.data);
3

Track changes

Queue all changes made offline:
// When user updates a field
await db.sync_queue.add({
  type: 'form_fill',
  data: {
    id: formFill.id,
    updated_at: formFill.updated_at,
    changes: { 'Building_Name': newValue }
  }
});
4

Detect connectivity

Monitor network status and trigger sync when online:
window.addEventListener('online', async () => {
  if (navigator.onLine) {
    await syncOfflineChanges();
  }
});
5

Sync changes

Send queued changes to server using POST /api/v1/sync:
const syncItems = await db.sync_queue.toArray();
const response = await fetch('/api/v1/sync', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ sync_items: syncItems })
});
6

Handle results

Process sync results and handle conflicts:
const { results } = await response.json();

// Remove successful items from queue
for (const item of results.success) {
  await db.sync_queue.delete(item.local_id);
}

// Show conflict resolution UI
if (results.conflicts.length > 0) {
  showConflictDialog(results.conflicts);
}

Photo handling offline

Photos require special handling for offline use:

Downloading photos

// Download and cache photo blobs
for (const photo of formFill.photos) {
  const blob = await fetch(photo.url).then(r => r.blob());
  await db.photos.put({
    id: photo.id,
    form_fill_id: formFill.id,
    blob: blob,
    filename: photo.filename
  });
}

Uploading photos when online

// Upload captured photos
const formData = new FormData();
formData.append('form_fill_id', formFillId);
formData.append('field_name', fieldName);
formData.append('photo', photoBlob, 'inspection_photo.jpg');

await fetch('/api/v1/sync/upload_photo', {
  method: 'POST',
  body: formData
});

Checksum validation

The sync_metadata.checksum field enables data integrity verification:
function validateChecksum(data, expectedChecksum) {
  const actualChecksum = md5(JSON.stringify(data));
  if (actualChecksum !== expectedChecksum) {
    console.warn('Data integrity check failed - re-download recommended');
    return false;
  }
  return true;
}
Always validate checksums after downloading offline data to ensure integrity, especially over unreliable network connections.

Performance considerations

Download size optimization

ComponentAverage SizeNotes
Inspection metadata~2 KBMinimal overhead
Form fill (no photos)~5-10 KBDepends on field count
Form structure~10-20 KBTemplate complexity
Photo (JPEG)200-500 KBCompressed images
Complete inspection1-5 MBWith 5-10 photos
For inspections with many photos, consider:
  • Downloading lower resolution thumbnails for offline viewing
  • Uploading full-resolution photos separately when online
  • Implementing progressive download (metadata first, then photos)

IndexedDB limits

Browser storage limits vary:
  • Chrome/Edge: ~60% of disk space (typically GBs)
  • Firefox: ~50% of disk space
  • Safari: ~1 GB with user prompt for more
Monitor storage usage:
if (navigator.storage && navigator.storage.estimate) {
  const { usage, quota } = await navigator.storage.estimate();
  const percentUsed = (usage / quota) * 100;
  console.log(`Storage: ${percentUsed.toFixed(2)}% used`);
}

Conflict resolution strategies

Implement appropriate conflict resolution based on your use case:

1. Server wins (safest)

if (result.conflict) {
  // Discard local changes, use server version
  await db.form_fills.put(result.conflict_data.server_data);
}

2. Client wins (force local)

if (result.conflict) {
  // Re-submit with force flag
  await syncWithResolve(item, 'use_local');
}
if (result.conflict) {
  // Show UI for user to choose or merge
  const choice = await showConflictDialog(
    result.conflict_data.server_data,
    result.conflict_data.local_data
  );
  
  if (choice === 'merge') {
    const merged = mergeData(
      result.conflict_data.server_data,
      result.conflict_data.local_data
    );
    await syncMergedData(merged);
  }
}

4. Field-level merge (advanced)

function mergeFormData(server, local) {
  const merged = { ...server };
  
  // Only override server fields that user actually changed
  for (const [key, value] of Object.entries(local)) {
    if (value !== server[key] && userModifiedFields.has(key)) {
      merged[key] = value; // Keep user's change
    }
  }
  
  return merged;
}

Best practices

Batch downloads

Download all scheduled inspections at once before going offline

Background sync

Use Service Worker Background Sync API for reliable uploads

Optimistic UI

Update UI immediately, sync in background

Sync indicators

Show clear offline/online status and pending sync count

Service Worker example

Implement Background Sync for reliable offline support:
// service-worker.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-inspections') {
    event.waitUntil(syncAllPendingChanges());
  }
});

async function syncAllPendingChanges() {
  const db = await openDB();
  const pending = await db.sync_queue.toArray();
  
  if (pending.length === 0) return;
  
  const response = await fetch('/api/v1/sync', {
    method: 'POST',
    body: JSON.stringify({ sync_items: pending })
  });
  
  const { results } = await response.json();
  
  // Clean up successful syncs
  for (const item of results.success) {
    await db.sync_queue.delete(item.local_id);
  }
}

Testing offline functionality

Test offline capabilities thoroughly:
// Simulate offline mode
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.controller.postMessage({
    type: 'SIMULATE_OFFLINE',
    enabled: true
  });
}

// Test sync after 'going online'
window.dispatchEvent(new Event('online'));
Chrome DevTools supports offline simulation via Network tab → Throttling → Offline. Use this for development and testing.

Build docs developers (and LLMs) love