_CORE
AI & Agentic Systems Core Information Systems Cloud & Platform Engineering Data Platform & Integration Security & Compliance QA, Testing & Observability IoT, Automation & Robotics Mobile & Digital Banking & Finance Insurance Public Administration Defense & Security Healthcare Energy & Utilities Telco & Media Manufacturing Logistics & E-commerce Retail & Loyalty
References Technologies Blog Know-how Tools
About Collaboration Careers
CS EN
Let's talk

Why Your Mobile App Doesn't Work Offline — And How to Fix It

20. 10. 2025 8 min read CORE SYSTEMSai
Why Your Mobile App Doesn't Work Offline — And How to Fix It

A user opens your app on the subway, on a plane, at a cabin without signal — and sees a spinner. Or an empty screen. Or an error message. In 2026, this is unacceptable. Offline-first isn’t a luxury — it’s the foundation of good mobile experience. And surprisingly, most teams are still doing it wrong.

Why Offline Matters

The statistics are clear: the average user spends 11% of their time without stable internet connection. Not because they live in rural areas — but because they ride subways, travel by plane, walk through concrete buildings with poor signal. And in those moments, your app either works, or the user replaces it with one that does.

Offline-first doesn’t mean “the app somehow survives without internet.” It means that local data is the primary source of truth and the network is a synchronization mechanism. This is a fundamental architectural shift — not a feature you stick on at the end.

For business applications (field service, logistics, inspections, retail), offline support is critical. A technician in the field must enter data even without signal. A driver must confirm delivery in an underground garage. An inspector must photograph a defect in a basement. If your app can’t do this, you have a problem.

Service Workers: The Foundation of Offline Web

For PWAs and web applications, Service Workers are the key technology. A Service Worker is a JavaScript file that runs in the background in the browser, independent of the main page. It functions as a proxy between the application and the network — intercepting HTTP requests and deciding whether to serve them from cache, network, or a combination of both.

// sw.js — Service Worker registration
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-shell-v2').then((cache) => {
      return cache.addAll([
        '/',
        '/css/app.css',
        '/js/app.js',
        '/offline.html'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((cached) => cached || fetch(event.request))
      .catch(() => caches.match('/offline.html'))
  );
});

Key concept: Service Worker survives tab closure. Once installed, it remains active and can serve requests even when the user is offline. This is a crucial difference from regular HTTP caching — you have full programmatic control over it.

IndexedDB vs SQLite: Where to Store Offline Data

Cache API handles static assets. But what about dynamic data — orders, forms, product catalogs? Here you have two main choices.

IndexedDB

Native browser API. Asynchronous, transactional NoSQL database directly in the browser. Ideal for PWAs. Capacity in hundreds of MB. Access through structured keys and indexes.

SQLite (WASM / native)

Full-featured SQL database. For native applications (React Native, Flutter, Swift, Kotlin) — direct access. For web through sql.js or OPFS. Strong with relational data and complex queries.

When to use what? For PWAs, IndexedDB is the natural choice — it’s built into the browser, doesn’t need WASM runtime, and has decent performance for most use cases. For native mobile applications, SQLite is the clear choice — it’s faster, has better support for transactions and complex queries, and is the de facto standard in the mobile world.

// IndexedDB — saving offline record
async function saveOfflineRecord(record) {
  const db = await openDB('fieldwork', 1, {
    upgrade(db) {
      const store = db.createObjectStore('inspections', {
        keyPath: 'id'
      });
      store.createIndex('synced', 'synced');
      store.createIndex('timestamp', 'timestamp');
    }
  });
  await db.put('inspections', {
    ...record,
    id: crypto.randomUUID(),
    synced: false,
    timestamp: Date.now()
  });
}

Important detail: never rely on localStorage for offline data. 5 MB limit, synchronous API (blocks main thread), and no support for indexes or transactions. localStorage is for user preferences, not business data.

Sync Strategies: Optimistic vs Pessimistic

The most complex part of offline-first architecture isn’t storing data — it’s synchronizing it back to the server when connection is restored. There are two fundamental approaches.

Optimistic Sync (Offline-First)

User performs an action, app immediately confirms it and writes to local database. Synchronization happens in the background as soon as network is available. If conflicts arise, they’re resolved retroactively — either automatically (last-write-wins, merge) or with user confirmation.

Advantages: immediate response, works without network, better UX. Disadvantages: conflicts, eventual consistency, more complex error handling. Usage: field service apps, notes, forms, ToDo lists, chat.

Pessimistic Sync (Online-First)

User performs an action, app waits for server confirmation and only then updates UI. Offline queue stores operations for later submission, but doesn’t present them as confirmed.

Advantages: consistency, no conflicts, simpler implementation. Disadvantages: slower UX, network dependency for confirmation. Usage: financial transactions, orders, medical records — anything where eventual consistency isn’t acceptable.

// Optimistic sync — offline queue with retry
class SyncManager {
  async enqueue(operation) {
    const queue = await this.getQueue();
    queue.push({
      id: crypto.randomUUID(),
      operation,
      timestamp: Date.now(),
      retries: 0,
      status: 'pending'
    });
    await this.saveQueue(queue);
    this.attemptSync();
  }

  async attemptSync() {
    if (!navigator.onLine) return;
    const queue = await this.getQueue();
    for (const item of queue.filter(i => i.status === 'pending')) {
      try {
        await this.sendToServer(item.operation);
        item.status = 'synced';
      } catch (e) {
        item.retries++;
        if (item.retries >= 3) item.status = 'failed';
      }
    }
    await this.saveQueue(queue);
  }
}

Background Sync API

Manual polling and online/offline events are fragile. Background Sync API is a browser API that handles synchronization elegantly: you register a sync event in Service Worker and the browser triggers it as soon as it has stable connection — even if the user closed the app.

// Register Background Sync from main app
async function scheduleSync() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('sync-inspections');
}

// Service Worker — handle sync event
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-inspections') {
    event.waitUntil(syncPendingInspections());
  }
});

async function syncPendingInspections() {
  const db = await openDB('fieldwork', 1);
  const pending = await db.getAllFromIndex(
    'inspections', 'synced', false
  );
  for (const record of pending) {
    const res = await fetch('/api/inspections', {
      method: 'POST',
      body: JSON.stringify(record)
    });
    if (res.ok) {
      record.synced = true;
      await db.put('inspections', record);
    }
  }
}

Important limitation: Background Sync API currently has full support only in Chromium browsers. Safari support is limited. For native apps, use platform equivalents — WorkManager (Android) or BGTaskScheduler (iOS).

Cache Strategies

Choosing the right cache strategy fundamentally affects app performance and offline behavior. There’s no single correct strategy — different content types require different approaches.

  • Cache First: Always serve from cache, use network only for updates. Ideal for static assets (CSS, JS, fonts, icons) that only change during deployment.
  • Network First: Try network, fallback to cache on failure. For API data where you want freshness but tolerate stale data offline.
  • Stale-While-Revalidate: Immediately serve from cache and simultaneously update cache from network in background. Best compromise between speed and freshness. Great for product catalogs, lists, profiles.
  • Network Only: No caching. For data that must always be current — auth tokens, real-time prices, transactions.
  • Cache Only: Only cache, no network. For pre-cached assets of app shell, offline fallback pages.
// Stale-While-Revalidate in Service Worker
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/products')) {
    event.respondWith(
      caches.open('api-cache').then(async (cache) => {
        const cached = await cache.match(event.request);
        const fetchPromise = fetch(event.request).then((res) => {
          cache.put(event.request, res.clone());
          return res;
        });
        return cached || fetchPromise;
      })
    );
  }
});

In practice, you combine strategies based on content type. App shell is Cache First, API endpoints are Stale-While-Revalidate or Network First, and authentication is Network Only. The key is having this explicitly defined in architectural documentation, not ad hoc in code.

Real-World Architecture of Offline-First Application

How do we put it all together? A typical production offline-first mobile application architecture has these layers:

  • UI Layer: React Native / Flutter / PWA. Reads data exclusively from local database. Never directly from network.
  • Local Storage Layer: SQLite (native) or IndexedDB (web). Structured local database with indexes, transactions, and migration support.
  • Sync Engine: Custom or library-based (WatermelonDB, PouchDB, PowerSync). Handles bidirectional sync, conflict resolution, retry logic, and delta sync.
  • Network Layer: HTTP/REST or GraphQL with offline queue. Interceptor detects online/offline state and routes requests.
  • Backend API: Server side with support for idempotent operations, conflict detection (vector clocks, timestamps), and bulk sync endpoints.

Critical detail most teams underestimate: conflict resolution. What happens when two users edit the same record offline? Last-write-wins is simplest, but not always correct. For complex scenarios, you need CRDTs (Conflict-free Replicated Data Types) or field-level merge with user confirmation.

How We Build It at CORE SYSTEMS

At CORE SYSTEMS, we deliver offline-first mobile applications for field service, logistics, and inspection processes — exactly those scenarios where reliable offline mode isn’t nice-to-have, but a hard requirement.

Our approach starts with data flow analysis: which entities must be available offline, what’s the expected data volume, what operations are performed without network, and what’s the tolerance for conflicts. Based on this, we design sync strategy — usually combining optimistic sync for creating new records with pessimistic approach for editing critical data.

Technologically, we reach for React Native + WatermelonDB for native applications or PWA + IndexedDB + Workbox for web solutions. Workbox from Google is a production-grade library for Service Worker strategies — handling precaching, runtime caching, and Background Sync with minimal boilerplate code.

Every offline-first app we deliver has: sync status indicator in UI (user always knows if they’re online/offline and how many operations await sync), conflict resolution UI for edge cases, automated sync testing in CI/CD pipeline, and monitoring dashboard for tracking sync health in production.

Conclusion: Offline-First Is an Architectural Decision

You can’t add offline support at the end of the development cycle. It’s an architectural decision that affects data model, sync logic, UX flow, and backend API design. The earlier you make it, the cheaper it costs you.

Good news: in 2026 we have mature tools — Service Workers, IndexedDB, Background Sync API, WatermelonDB, Workbox, PowerSync. Technology isn’t the blocker. The blocker is architectural thinking that still assumes internet is always available. It’s not.

Share:

CORE SYSTEMS

Stavíme core systémy a AI agenty, které drží provoz. 15 let zkušeností s enterprise IT.

Need help with implementation?

Our experts can help with design, implementation, and operations. From architecture to production.

Contact us