Implementing Offline Mode for Desktop Applications
Offline mode isn't just "show a placeholder when no network". It's architectural: the app works fully without network and syncs when it reconnects. Between states — operation queue, conflict resolution, and honest status display.
Network State Detection
// main/network-monitor.js
const { net } = require('electron');
class NetworkMonitor {
constructor() {
this.isOnline = true;
this.checkInterval = null;
}
start(mainWindow) {
this.window = mainWindow;
this.checkInterval = setInterval(() => this.checkConnectivity(), 15000);
this.checkConnectivity();
}
async checkConnectivity() {
const wasOnline = this.isOnline;
try {
const response = await Promise.race([
fetch('https://api.your-app.com/ping', { method: 'HEAD' }),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
]);
this.isOnline = response.ok;
} catch {
this.isOnline = false;
}
if (wasOnline !== this.isOnline) {
this.window?.webContents.send('network:change', { isOnline: this.isOnline });
}
}
stop() {
clearInterval(this.checkInterval);
}
}
module.exports = new NetworkMonitor();
Local Database: SQLite
// main/db.js
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');
const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
updated_at INTEGER NOT NULL,
server_updated_at INTEGER,
sync_status TEXT NOT NULL DEFAULT 'synced'
);
CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0
);
`);
module.exports = db;
Optimistic Updates Pattern
// main/documents.js
const db = require('./db');
function createDocument(doc) {
const id = doc.id || crypto.randomUUID();
const now = Date.now();
// Write locally immediately
db.prepare(`
INSERT INTO documents (id, title, content, updated_at, sync_status)
VALUES (@id, @title, @content, @updated_at, @sync_status)
`).run({
id, title: doc.title, content: doc.content,
updated_at: now, sync_status: 'pending'
});
// Add to sync queue
db.prepare(`
INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at)
VALUES ('create', 'document', @id, @payload, @created_at)
`).run({
id, payload: JSON.stringify(doc), created_at: now
});
return { id, title: doc.title, content: doc.content, sync_status: 'pending' };
}
module.exports = { createDocument };
Sync Process
// main/sync.js
const db = require('./db');
const networkMonitor = require('./network-monitor');
class SyncManager {
constructor() {
this.isSyncing = false;
networkMonitor.on('change', (isOnline) => {
if (isOnline) this.sync();
});
}
async sync() {
if (this.isSyncing || !networkMonitor.isOnline) return;
this.isSyncing = true;
try {
await this.pushPendingOperations();
await this.pullRemoteChanges();
} finally {
this.isSyncing = false;
}
}
async pushPendingOperations() {
const pending = db.prepare(`
SELECT * FROM sync_queue WHERE attempts < 3 ORDER BY created_at LIMIT 50
`).all();
for (const item of pending) {
try {
await this.executeOperation(item);
db.prepare('DELETE FROM sync_queue WHERE id = ?').run(item.id);
db.prepare(`
UPDATE documents SET sync_status = 'synced' WHERE id = ?
`).run(item.entity_id);
} catch (error) {
db.prepare(`
UPDATE sync_queue SET attempts = attempts + 1 WHERE id = ?
`).run(item.id);
}
}
}
async executeOperation(item) {
const token = await getAuthToken();
const response = await fetch(`https://api.your-app.com/${item.entity_type}s`, {
method: item.operation === 'create' ? 'POST' : 'PUT',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: item.payload
});
if (!response.ok) throw new Error(`API error ${response.status}`);
return response.json();
}
}
module.exports = new SyncManager();
Sync Status Indicators
Users should always understand if their data is saved:
- Cloud icon with X = no network, unsaved changes
- Cloud with arrow = syncing
- Cloud with checkmark = synced
- Warning icon = conflicts
Status must be in constantly visible UI — header or footer.
Full offline implementation with conflicts, queue, and sync takes 3-5 days for basic functionality.







