Production-grade offline-first mobile prototype for construction material delivery field-capture, built with real SQLite local persistence, automatic sync, restart recovery, and PostgreSQL deduplication.
FieldCapture is an offline-first mobile application designed specifically for construction job sites with intermittent or zero cellular connectivity. Construction foremen frequently receive vital material deliveries (rebar, structural steel, ready-mix concrete, masonry, lumber) in deep excavations, concrete basements, or remote zones where network connectivity is unavailable.
FieldCapture enables foremen to immediately photograph physical delivery tickets, enter supplier and purchase order data, log delivery notes, and store them locally on the device with 100% guarantee of data preservation. When network connectivity is restored, the dedicated Sync Manager automatically transmits queued payloads and photos to the backend PostgreSQL database using idempotency keys to strictly prevent duplicate entries.
- Genuine Offline-First Persistence: Powered by a local SQLite database (Room). Deliveries are always written to the local database before any network activity is attempted.
- Permanent Local Photo Storage: Photos of delivery tickets are copied directly to application-sandboxed persistent storage and stored as local file URIs, guaranteeing they render on-device even in airplane mode.
- Dedicated Sync Manager: Automatically detects network availability (Android
ConnectivityManagercallbacks) and transitions queued records toUPLOADINGandSYNCEDupon connection return. - Interrupted Upload & App-Restart Recovery: If the application is terminated while a delivery is actively uploading (
UPLOADINGstatus), the database initialization automatically recovers the record back toQUEUEDwith an audit error explanation, preventing lost deliveries. - Duplicate-Submission & Idempotency Protection:
- UI-level submission throttling prevents double-taps.
- Every delivery record generates a cryptographically unique
idempotencyKey(UUIDv4). - The PostgreSQL database enforces
UNIQUE(idempotency_key). When an already-processed delivery is re-transmitted (e.g. following network timeouts or retries), the server returns the existing record rather than creating a duplicate.
- Offline Queue & Detailed Status: Clear statuses (
QUEUED,UPLOADING,SYNCED,FAILED), filterable tabs, error diagnostics, and one-tap retry for individual records or entire queues. - Interactive Developer / Demo Testing Suite: Built directly into the mobile interface to allow rapid assessment of:
- Forced Offline Mode
- Forced Online Mode
- Simulated HTTP 500 Outage
- App-Restart Crash Recovery
┌────────────────────────────────────────────────────────┐
│ Mobile Client │
│ │
│ [Record Delivery UI] │
│ │ │
│ ▼ │
│ [Local SQLite Database (Room)] ◄─── Source of Truth │
│ │ │
│ ▼ │
│ [Offline Sync Manager] ◄──── [ConnectivityManager] │
│ │ │
└─────────┼──────────────────────────────────────────────┘
│ (Multipart POST with idempotencyKey + photo)
▼
┌────────────────────────────────────────────────────────┐
│ Backend Service │
│ │
│ [Node.js + Express + TypeScript API] │
│ │ │
│ ├── Photo Storage: Vercel Blob (ticket images) │
│ │ │
│ ▼ │
│ [PostgreSQL Database] │
│ deliveries table (UNIQUE idempotency_key) │
└────────────────────────────────────────────────────────┘
In an offline-first architecture, treating the remote API as the primary data store leads to brittle user experiences, lost input, and UI freezing. In FieldCapture:
- Local write guarantee: User actions complete immediately and durably in local SQLite within milliseconds regardless of radio conditions.
- Decoupled synchronization: The Sync Manager operates as an independent background worker that reacts to database changes and network lifecycle events.
- Auditability: Local sync attempts, timestamps, and error messages are recorded per delivery, giving site personnel clear feedback on delivery synchronization state.
├── app/ # Mobile Application (Android / Kotlin / Jetpack Compose)
│ ├── src/main/java/com/example/
│ │ ├── MainActivity.kt # App entrypoint & navigation state
│ │ ├── data/
│ │ │ ├── model/Delivery.kt # Room Entity, DeliveryStatus, DeliveryCounts
│ │ │ ├── database/ # FieldCaptureDatabase, DeliveryDao, Converters
│ │ │ ├── repository/ # DeliveryRepository
│ │ │ └── storage/ # PhotoStorageService (Local image persistence & ticket generator)
│ │ ├── sync/
│ │ │ ├── NetworkMonitor.kt # Network capabilities listener & test simulation modes
│ │ │ ├── SyncManager.kt # Background sync queue, retry, and crash recovery
│ │ │ └── ApiClient.kt # HTTP client with idempotency handling
│ │ └── ui/
│ │ ├── MainViewModel.kt # Reactive state management
│ │ ├── components/ # NetworkStatusBar, DeliveryStatusBadge, DeliveryItemCard
│ │ ├── screens/ # HomeScreen, RecordDeliveryScreen, QueueScreen, DeliveryDetailScreen
│ │ └── theme/ # High-contrast industrial design palette
│ └── build.gradle.kts
│
├── server/ # Backend REST API (Node.js + Express + TypeScript)
│ ├── src/
│ │ ├── index.ts # Express server entrypoint & health checks
│ │ ├── db.ts # PostgreSQL connection pool & schema bootstrapper
│ │ └── routes/deliveries.ts # Idempotent POST /api/deliveries & GET routes
│ ├── schema.sql # DDL for PostgreSQL table & indexes
│ ├── package.json
│ ├── tsconfig.json
│ └── .env.example
│
├── .env.example # Combined environment variable references
└── README.md
The mobile application is ready to run directly in the AI Studio cloud streaming emulator or locally in Android Studio.
To build and compile:
gradle assembleDebug-
Navigate to the server folder:
cd server npm install -
Configure environment:
cp .env.example .env # Edit DATABASE_URL and PORT if needed -
Ensure PostgreSQL is running and initialize schema:
psql -U postgres -d fieldcapture -f schema.sql
-
Start development server:
npm run dev
The server exposes:
- Health check:
GET http://localhost:3000/api/health - Create delivery:
POST http://localhost:3000/api/deliveries - List deliveries:
GET http://localhost:3000/api/deliveries - Fetch by ID:
GET http://localhost:3000/api/deliveries/:id
- Import the repository into Vercel and set Root Directory to
server. - In the Vercel project, open Storage, create a Blob store with Public access, and connect it to the project for Production. Vercel provides the Blob credentials to the deployment.
- Add
DATABASE_URLin the Vercel project's Environment Variables. Use the pooled Neon connection string for the API. - Deploy the project. Check
https://<deployment-domain>/api/healthand confirm the database reportsconnected. - Set
API_BASE_URLin the repository-root.envto the deployed HTTPS domain, then rebuild the APK. The app reads this value from generatedBuildConfig.
The API compresses ticket photos on Android and accepts files up to 3 MB to stay below Vercel Functions' 4.5 MB request-body limit. Public Blob URLs can be opened by anyone who has the URL; do not use this public store for sensitive ticket images.
Vercel Hobby currently includes 1 GB Blob storage, 10,000 simple operations, 2,000 advanced operations, and 10 GB Blob transfer per month. Hobby is for personal, non-commercial use; check Vercel's current terms before using it for a business/jobsite workflow.
- In the app, open the Demo & Assessment Test Controls (tap the tuning icon in the top right).
- Select Force Offline. Observe the top status bar change to Offline with an amber indicator.
- Tap Record Material Delivery. Notice the top banner states: "You're offline — Your delivery will be saved on this device and synced automatically when you're back online."
- Tap Capture Ticket (or choose from device gallery). Enter Supplier (e.g.
Vulcan Materials), PO Number (e.g.PO-4481-B), and delivery notes. - Tap Save Delivery. The app immediately transitions to the Offline Queue.
- Verify status displays QUEUED ("Waiting for connection").
- Return to the Home screen or keep the Queue screen open.
- Under Test Controls, toggle network mode to Force Online (or Auto).
- The Network Monitor automatically detects connectivity. The Sync Manager instantly transitions the delivery status from QUEUED → UPLOADING (with progress indicator) → SYNCED (green checkmark).
- Tap on the delivery to open Delivery Details. Notice the assigned PostgreSQL Server ID, the Idempotency Key, and confirmation that the item is permanently synchronized.
- Under Test Controls, tap Test Crash Recovery.
- This simulates an application process being killed while an upload is in the
UPLOADINGstate. - The database recovery query executes: any delivery stuck in
UPLOADINGis safely reverted toQUEUEDwith the message: "Upload interrupted by app restart — queued for retry". - Once online, the Sync Manager picks it up and retries the upload without data loss.
- The Save Delivery button disables during local write operations to prevent double-clicks.
- Every delivery has a client-generated UUIDv4
idempotencyKey. - If an upload is retried or resent, the backend inspects
idempotency_keyagainst PostgreSQL. - When a match is found, the backend returns the existing delivery with
isDuplicate: trueand does not insert an additional row.
- Functional mobile app with Kotlin, Jetpack Compose, and Room (SQLite)
- Local photo capture with persistent file storage and offline rendering
- Dedicated Sync Manager with automatic retry on network reconnection
- Edge-case handling for app killed mid-upload with state recovery
- Client and server idempotency duplicate protection
- Clean Node.js + Express + TypeScript backend with PostgreSQL schema
- High-contrast enterprise Material 3 design optimized for field visibility