Blog Offline Apps

How does an app work with and without internet?

Local storage, synchronization, and the magic behind offline-first applications

Have you ever wondered how apps like WhatsApp, Spotify, or Google Maps can show information even when you don't have internet? The secret is in local storage and smart synchronization. Let me explain how it works in simple terms.

Offline-First Synchronization Flow

1. User action

Without internet

Pending

2. Local storage

Saved on device

Saved

3. Sync when online

Sends to server

Synced
Offline
Local
Online

How does an app save data without internet?

LocalStorage

Key-Value

Stores simple data as key-value pairs (text, numbers, settings). Maximum 5-10 MB. Perfect for user preferences, themes, or login tokens.

// Save data
localStorage.setItem('theme', 'dark');

// Get data
const theme = localStorage.getItem('theme');

IndexedDB

Relational

Full NoSQL database inside the browser. Stores large amounts of structured data (products, messages, images). No size limit. Perfect for offline-first apps.

// Open database
const db = await indexedDB.open('myDB', 1);

// Store data
const transaction = db.transaction(['store'], 'readwrite');

SQLite

Native Apps

Embedded SQL database used in iOS and Android native apps. Fast, reliable, supports complex queries. WhatsApp, Telegram, and many apps use it.

-- SQL query example
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  name TEXT,
  sync_status INTEGER
);
Sync Status

How does the app know what to sync?

Every record saved locally has a "status" flag that tells the app whether it has been synchronized with the server

pending

Pending synchronization

The data was created or modified without internet. It's saved locally but not yet on the server.

syncing

Synchronizing

The app detected internet and is sending pending data to the server.

synced

Synchronized

The data is on both the device and the server. It's backed up and accessible from anywhere.

error

Sync error

Something went wrong (no connection, server error). The app will retry later.

// Example of a record with sync status
{
  "id": 123,
  "name": "Client name",
  "amount": 1500,
  "sync_status": "pending",  // pending, syncing, synced, error
  "created_at": "2026-05-12T10:00:00Z",
  "updated_at": "2026-05-12T10:00:00Z"
}

Types of apps: how each one handles offline

PWA

Uses Service Worker + Cache API + IndexedDB. Works on any device with a browser. You can install it on your home screen. Cross-platform by nature.

Web Android iOS

Native App

Uses SQLite or Realm. Separate development for iOS (Swift) and Android (Kotlin). Full device access, best performance, but more expensive.

Swift Kotlin

Cross-Platform

React Native, Flutter, or Ionic. One codebase for iOS + Android + Web (sometimes). Can use SQLite or AsyncStorage. Balance between cost and performance.

React Native Flutter

A daily life example

WhatsApp

When you send a message without internet:

  1. The message is saved locally with status "pending"
  2. You see a clock icon (pending)
  3. When you reconnect, the app sends all pending messages
  4. The icon changes to two check marks (sent and received)

Spotify

Downloaded songs are stored locally using IndexedDB or native file system:

  1. You download songs while online
  2. Files are saved with "synced" status
  3. Offline: play directly from local storage

Simple code example: saving with status

// Function to save data offline
async function saveSale(product, amount) {
  const sale = {
    id: Date.now(),
    product: product,
    amount: amount,
    sync_status: 'pending',  // not synced yet
    created_at: new Date().toISOString()
  };
  
  // Save to local database (IndexedDB)
  await localDB.save('sales', sale);
  
  // Try to sync if online
  if (navigator.onLine) {
    await syncPendingSales();
  }
  
  return sale;
}

// Sync when internet comes back
window.addEventListener('online', () => {
  syncPendingSales();
});

// Send pending data to server
async function syncPendingSales() {
  const pending = await localDB.get('sales', { sync_status: 'pending' });
  
  for (const sale of pending) {
    sale.sync_status = 'syncing';
    await localDB.update('sales', sale);
    
    try {
      await fetch('/api/sales', {
        method: 'POST',
        body: JSON.stringify(sale)
      });
      sale.sync_status = 'synced';
      await localDB.update('sales', sale);
    } catch (error) {
      sale.sync_status = 'error';
      await localDB.update('sales', sale);
    }
  }
}

Share this article