Built a production-grade offline-first mobile time tracking application for field service technicians using React Native and Expo. The app enables workers in areas with unreliable connectivity to clock in/out, track hours across multiple rate types, and automatically sync data when network access is restored—all while maintaining ACID-compliant data integrity and zero data loss guarantees.
Field technicians frequently work in areas without reliable cellular coverage (basements, rural job sites, construction zones). The system needed to guarantee that every clock-in/clock-out action was captured and eventually synchronized to the server, even if the app crashed or the device lost power. Traditional AsyncStorage-based solutions lack transaction support and cannot provide ACID guarantees for critical time-tracking data.
Unlike typical SaaS mobile apps that authenticate to a single organization, this app needed to support employees who work for multiple client organizations through a staffing agency model. The authentication flow had to handle tenant-specific API endpoints while maintaining secure token storage and automatic session management across app restarts.
Compliance requirements demanded GPS coordinate capture at clock-in and clock-out events to validate on-site presence. The challenge involved balancing location accuracy (higher accuracy drains battery), handling permission denial gracefully, and implementing geofencing logic that accounts for GPS drift and variable accuracy readings from different device hardware.
When multiple offline actions occur before sync (clock-in, clock-out, rate changes), the system needed deterministic conflict resolution. Server-wins strategy was chosen to prevent client-side time manipulation, but this required careful UX design to avoid confusing users when their offline edits are overwritten during sync.
Trade-off: SQLite database with ACID transactions vs. AsyncStorage key-value store.
Rationale: AsyncStorage provides simple key-value persistence but lacks transaction support, indexing, and has a 6MB size limit on some Android devices. SQLite provides ACID guarantees (critical for financial time-tracking data), indexed queries for performance, and unlimited storage. The overhead of SQLite (additional 2MB native binary) was justified by reliability requirements.
Result: Zero data loss even with forced app termination. Average queue operation completes in <5ms. Support for 1000+ queued items without performance degradation.
Trade-off: Redux (industry standard) vs. Zustand (lightweight alternative).
Rationale: Redux requires significant boilerplate (actions, reducers, middleware) and adds 20KB to bundle size. Zustand provides the same functionality with 1KB footprint and TypeScript-first design. For a mobile app where bundle size directly impacts download and startup time, the 95% size reduction was significant. Team familiarity with Redux was sacrificed for better performance.
Implementation: Three isolated stores (authStore, punchStore, offlineStore) with AsyncStorage persistence middleware. Each store manages a single bounded context to prevent coupling.
Trade-off: Manual fetch/cache management vs. React Query library.
Rationale: Server state (work orders, time entries, employee data) requires different patterns than local UI state. React Query provides automatic background refetching, stale-while-revalidate caching, and request deduplication. This eliminated 200+ lines of manual cache invalidation logic and prevented N+1 fetch patterns when navigating between screens.
Configuration: AsyncStorage persistence layer ensures work orders remain available offline. 5-minute stale time balances freshness with API call reduction.
Trade-off: Expo's managed workflow vs. bare React Native with manual native configuration.
Rationale: Expo provides zero-config access to native APIs (GPS, camera, secure storage, SQLite) without writing native code. Over-the-air updates enable hotfix deployment without app store approval. The trade-off is larger base bundle size (+8MB) and inability to use certain native libraries. For a field service app where rapid iteration trumps bundle optimization, Expo's developer velocity benefits outweighed costs.
Result: Built MVP in days instead of weeks. OTA updates deployed in minutes vs. 2-day app store review cycle.
Implemented repository pattern with SQLite backend that queues time entry operations when offline. Each queued item includes operation type (create/update), retry count, and last error message. Upon network restoration, the sync service processes the queue sequentially with exponential backoff (2s, 4s, 8s intervals) up to 3 retry attempts. Failed items remain in queue with error details exposed through UI for manual resolution. Haptic feedback provides tactile confirmation of sync events.
Built Axios interceptor that checks network connectivity before every API request. If offline, requests automatically queue to SQLite instead of failing. Token authentication happens at interceptor level with automatic injection of tenant-specific headers. 401 responses trigger automatic logout and credential clearing to prevent stale token issues. Request timeout set to 15 seconds to fail fast on poor connections rather than blocking UI.
Clock-in/clock-out actions update UI immediately before server confirmation to eliminate perceived latency. Active timer starts counting in real-time even when request is queued offline. If sync eventually fails due to validation errors (invalid work order, missing rate), UI rolls back optimistic changes and displays error banner. This pattern provides instant feedback while maintaining data integrity.
Location service requests high-accuracy GPS coordinates asynchronously. While GPS fix is acquired (can take 3-30 seconds), clock-in proceeds immediately using cached last-known location if available. Accuracy threshold of 50 meters filters out poor GPS readings. Geofence validation compares GPS coordinates against work order site location using Haversine formula for great-circle distance. Results stored as boolean flags (in_zone/in_zone_end) rather than failing the clock-in to avoid blocking workers.
State stores implement singleton pattern with persistence to AsyncStorage for hydration across app restarts. Auth store handles token storage in Expo SecureStore (encrypted keychain on iOS, AES256 on Android) separate from general AsyncStorage. Offline store maintains pending sync count for UI badge display and last sync timestamp for debugging. Store selectors prevent unnecessary re-renders through shallow equality checks.
Work orders and rates cached with React Query and persisted to AsyncStorage using AsyncStorage persister. Stale-while-revalidate pattern serves cached data instantly while fetching fresh data in background. Garbage collection removes unused queries after 24 hours to prevent storage bloat. Prefetching on work order list triggers detail screen data load to eliminate loading spinners when user taps into detail view.
All API responses validated using Zod schemas to catch backend contract violations at runtime. TypeScript interfaces derived from Zod schemas ensure compile-time type safety matches runtime validation. This caught multiple backend inconsistencies during integration (nullable fields not marked optional, enum values changing without versioning). API client throws strongly-typed errors distinguishing network failures, validation errors, and business logic errors for appropriate UI handling.
Offline banner uses native slide-down animation with spring physics matching iOS/Android platform conventions. Haptic feedback provides tactile confirmation for critical actions (clock-in success, sync complete, errors) using platform-specific intensity patterns. Loading states use skeleton screens instead of spinners to reduce perceived latency. Error states display actionable messages with retry buttons rather than generic error codes.
- Achieved [100%] data capture rate with zero data loss even in areas with intermittent connectivity
- Reduced time entry submission latency from [30s] (web app) to [<100ms] (instant optimistic UI)
- [X%] reduction in GPS-related compliance violations through automated geofence validation
- Eliminated [X] hours/week of manual timesheet correction through automated rate tracking
- App bundle size maintained at [<25MB] despite offline capabilities through selective dependency management
- Sync success rate of [99.X%] with failed items surfaced for manual resolution
- Average API response time [<200ms] for cached queries through React Query optimization
- Support for [1000+] queued offline actions without performance degradation
Building offline-first apps demands accepting eventual consistency and designing for network partitions from day one. It's not sufficient to add offline support as an afterthought—the entire data flow must account for divergent client/server state. The critical insight is treating the local database as the source of truth and the server as a sync target, inverting the typical request-response model. This requires explicit conflict resolution strategies and UI that communicates sync status clearly.
For time tracking (financial data), ACID guarantees are mandatory. AsyncStorage's lack of transaction support means multi-step operations can partially complete, leading to inconsistent state if the app crashes. SQLite's transaction rollback on error prevents orphaned clock-ins without matching clock-outs. The performance cost of SQLite over AsyncStorage was negligible (<5ms queue operations), making it an easy choice for reliability-critical data.
Every megabyte of bundle size reduces install conversion on metered cellular connections. Zustand's 95% size reduction vs Redux, Expo's tree-shaking, and selective React Query features saved 15MB total bundle size. This translated to measurably higher install completion rates on cellular vs WiFi. Mobile users are far more sensitive to download size than web users, making dependency auditing a first-class concern.
Instant UI feedback (optimistic updates) dramatically improves perceived performance, but creates UX debt if sync fails. The key is designing rollback experiences that don't confuse users—showing clear error messages, preserving user intent where possible, and providing manual retry options. Haptic feedback helps communicate state changes that might otherwise be invisible when sync happens in background.
High-accuracy GPS can take 30+ seconds to acquire a fix indoors or in urban canyons with poor satellite visibility. Blocking clock-in on GPS acquisition would frustrate users, so using cached location with accuracy flags was necessary. Geofence validation must account for GPS drift (±10-50 meters typical) and handle graceful degradation when accuracy is poor. Treating geofence as advisory rather than blocking was critical for user acceptance.
When offline actions eventually sync, client-side modifications could potentially inflate hours worked. Implementing server-wins for conflicts (where server timestamp/duration overrides client values) prevents this attack vector while accepting the UX trade-off of occasionally overwriting legitimate offline edits. Clear messaging about which values will be preserved during sync sets appropriate expectations.
Over-the-air update capability enables hotfix deployment in minutes vs. days for app store review. This fundamentally changes risk tolerance for shipping—bugs are less catastrophic when they can be patched immediately. However, OTA updates don't update native code, so architectural decisions that minimize native changes (choosing Expo SDK over custom native modules) preserve this capability.
Runtime validation with Zod caught numerous backend contract violations that TypeScript alone would miss (nullable fields, enum changes, missing pagination metadata). The double validation (compile-time TypeScript + runtime Zod) creates defense in depth that pays for itself in prevented production issues. API contract testing should be bilateral—mobile client validates backend responses just as backend validates client requests.