Offline-First Architecture Design Patterns
Whistl works fully offline—protection doesn't depend on internet connectivity. This technical guide explains offline-first architecture, local data storage, conflict resolution, and how Whistl maintains continuous protection even in airplane mode.
Why Offline-First?
Financial protection can't depend on connectivity:
- Subways/planes: No signal during commute or travel
- Rural areas: Spotty coverage outside cities
- Network congestion: Events, emergencies overload networks
- Latency: Round-trip to server adds delay
- Privacy: Less data transmission = more privacy
Whistl's offline-first design ensures protection works anywhere, anytime.
Architecture Overview
All core functionality runs locally on device:
Local vs. Cloud Components
┌─────────────────────────────────────────────────┐ │ Local (On-Device) │ │ ┌─────────────────────────────────────────┐ │ │ │ ML Models (Impulse Prediction) │ │ │ │ - Neural network inference │ │ │ │ - Risk calculation │ │ │ └─────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────┐ │ │ │ Local Database (SQLCipher) │ │ │ │ - Transactions │ │ │ │ - Settings │ │ │ │ - Goals │ │ │ └─────────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────┐ │ │ │ Blocking Engine │ │ │ │ - DNS filtering │ │ │ │ - Geofencing │ │ │ └─────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────┤ │ Cloud (Optional Sync) │ │ - Backup storage │ │ - Multi-device sync │ │ - Model updates │ └─────────────────────────────────────────────────┘
Local Data Storage
All data is stored locally with encryption:
Database Schema
-- Transactions table
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
amount REAL NOT NULL,
merchant TEXT,
mcc TEXT,
category TEXT,
date TIMESTAMP NOT NULL,
location_lat REAL,
location_lng REAL,
risk_score REAL,
synced INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Settings table
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
encrypted INTEGER DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Goals table
CREATE TABLE goals (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
image_url TEXT,
target_amount REAL,
current_amount REAL,
deadline DATE,
priority INTEGER,
synced INTEGER DEFAULT 0
);
-- Risk patterns table
CREATE TABLE risk_patterns (
id TEXT PRIMARY KEY,
pattern_type TEXT,
pattern_data TEXT, -- JSON
confidence REAL,
last_triggered TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Data Access Layer
class LocalDatabase {
private let db: SQLiteDatabase
// Read transactions (always available offline)
func getTransactions(
from: Date,
to: Date,
category: String? = nil
) throws -> [Transaction] {
var query = "SELECT * FROM transactions WHERE date BETWEEN ? AND ?"
var params: [Any] = [from, to]
if let category = category {
query += " AND category = ?"
params.append(category)
}
query += " ORDER BY date DESC"
return try db.query(query, params: params)
.map { row in Transaction(from: row) }
}
// Insert transaction (queued for sync)
func insertTransaction(_ transaction: Transaction) throws {
try db.execute("""
INSERT INTO transactions
(id, amount, merchant, mcc, category, date, synced)
VALUES (?, ?, ?, ?, ?, ?, 0)
""", params: [
transaction.id,
transaction.amount,
transaction.merchant,
transaction.mcc,
transaction.category,
transaction.date
])
}
}
Offline Protection Features
Core protection works without internet:
Always-Available Features
| Feature | Offline Support | Notes |
|---|---|---|
| DNS Blocking | Full | Blocklist stored locally |
| Geofencing | Full | GPS works without internet |
| ML Inference | Full | On-device neural network |
| SpendingShield | Full | Local risk calculation |
| Transaction History | Full | Stored in local database |
| Goals/Dream Board | Full | Images cached locally |
| Partner Messaging | Limited | Queued for when online |
| Bank Sync | None | Requires internet |
Sync Strategy
When connectivity returns, data syncs automatically:
Sync Queue
class SyncQueue {
private let queue: OperationQueue
private var pendingOperations: [SyncOperation] = []
func enqueue(_ operation: SyncOperation) {
pendingOperations.append(operation)
// Process queue when online
if NetworkMonitor.shared.isOnline {
processQueue()
}
}
private func processQueue() {
for operation in pendingOperations {
queue.addOperation(operation)
}
pendingOperations = []
}
// Sync operations
func syncTransactions() {
enqueue(TransactionSyncOperation())
}
func syncSettings() {
enqueue(SettingsSyncOperation())
}
func syncGoals() {
enqueue(GoalsSyncOperation())
}
}
Sync Priority
enum SyncPriority: Int, Comparable {
case critical = 0 // Blocking state, intervention results
case high = 1 // Settings changes, new goals
case normal = 2 // Transaction history, patterns
case low = 3 // Analytics, model telemetry
static func < (lhs: SyncPriority, rhs: SyncPriority) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
class SyncScheduler {
func scheduleSync(_ operation: SyncOperation) {
// Critical operations sync immediately
if operation.priority == .critical {
operation.start()
return
}
// Lower priority operations batched
syncQueue.enqueue(operation)
}
}
Conflict Resolution
When multiple devices modify data, conflicts are resolved:
Conflict Detection
class ConflictDetector {
func detectConflicts(local: SyncData, remote: SyncData) -> [Conflict] {
var conflicts: [Conflict] = []
// Compare by entity ID and timestamp
for localEntity in local.entities {
if let remoteEntity = remote.entities.first(where: { $0.id == localEntity.id }) {
if localEntity.updatedAt != remoteEntity.updatedAt {
conflicts.append(Conflict(
entityType: localEntity.type,
entityId: localEntity.id,
localVersion: localEntity,
remoteVersion: remoteEntity
))
}
}
}
return conflicts
}
}
Resolution Strategies
| Data Type | Strategy | Description |
|---|---|---|
| Settings | Last-write-wins | Most recent change takes precedence |
| Goals | Merge | Combine progress from both devices |
| Transactions | Union | Add missing transactions from both |
| Patterns | Merge | Combine pattern data |
| Intervention Results | Union | All results preserved |
Network State Management
App responds gracefully to network changes:
Network Monitor
import Network
class NetworkMonitor {
static let shared = NetworkMonitor()
private let monitor = NWPathMonitor()
private let queue = DispatchQueue(label: "NetworkMonitor")
var isOnline: Bool { currentPath.status == .satisfied }
var isExpensive: Bool { currentPath.isExpensive } // Cellular
var isConstrained: Bool { currentPath.isConstrained } // Low data mode
private var currentPath: NWPath = NWPath()
var statusHandler: ((NetworkStatus) -> Void)?
func start() {
monitor.pathUpdateHandler = { [weak self] path in
self?.currentPath = path
self?.statusHandler?(NetworkStatus(from: path))
}
monitor.start(queue: queue)
}
func stop() {
monitor.cancel()
}
}
UI State Updates
class NetworkStateViewModel: ObservableObject {
@Published var isOnline: Bool = true
@Published var syncPending: Bool = false
@Published var lastSyncDate: Date?
init() {
NetworkMonitor.shared.statusHandler = { [weak self] status in
DispatchQueue.main.async {
self?.isOnline = status.isOnline
self?.updateSyncStatus()
}
}
}
private func updateSyncStatus() {
if isOnline && SyncQueue.shared.hasPendingOperations {
syncPending = true
SyncQueue.shared.processQueue()
} else {
syncPending = false
}
}
}
Offline Indicators
Users are informed of offline status:
UI Indicators
- Status bar: Orange indicator when offline
- Sync badge: Shows pending sync count
- Toast notification: "You're offline. Changes will sync when connected."
- Disabled features: Greyed out when unavailable
Offline Banner
struct OfflineBanner: View {
@ObservedObject var viewModel: NetworkStateViewModel
var body: some View {
if !viewModel.isOnline {
HStack {
Image("wifi-off")
.foregroundColor(.white)
Text("You're offline. Protection is still active.")
.font(.caption)
.foregroundColor(.white)
Spacer()
if viewModel.syncPending {
Text("\(SyncQueue.shared.pendingCount) pending")
.font(.caption2)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.white.opacity(0.3))
.cornerRadius(4)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.orange)
}
}
}
Caching Strategy
Resources are cached for offline access:
Cache Configuration
- Goal images: Cached indefinitely
- Blocklist: Cached, updated weekly
- ML models: Cached, updated monthly
- Transaction history: Last 90 days cached
- Settings: Cached indefinitely
Image Caching
class ImageCache {
private let cache = NSCache()
private let fileManager = FileManager.default
func getImage(for url: URL) -> UIImage? {
// Check memory cache
if let cached = cache.object(forKey: url.absoluteString as NSString) {
return cached
}
// Check disk cache
let path = getCachePath(for: url)
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let image = UIImage(data: data) {
cache.setObject(image, forKey: url.absoluteString as NSString)
return image
}
return nil
}
func cacheImage(_ image: UIImage, for url: URL) {
cache.setObject(image, forKey: url.absoluteString as NSString)
let path = getCachePath(for: url)
try? image.jpegData(compressionQuality: 0.8)?
.write(to: URL(fileURLWithPath: path))
}
}
Testing Offline Scenarios
Offline functionality is thoroughly tested:
Test Scenarios
- Airplane mode: All features without any connectivity
- Intermittent connection: Switching between online/offline
- Slow network: 2G/3G simulation
- Long offline period: Days without sync
- Multi-device conflict: Changes on multiple devices while offline
Conclusion
Whistl's offline-first architecture ensures protection works anywhere—subways, planes, rural areas, or anywhere with poor connectivity. All core features (blocking, ML inference, risk calculation) run locally, while sync happens automatically when connectivity returns.
Your protection doesn't depend on internet—Whistl works when and where you need it.
Get Always-On Protection
Whistl works fully offline—protection doesn't depend on internet. Download free and experience reliable offline-first protection.
Download Whistl FreeRelated: Local Storage Encryption | DNS Filtering | Cloud Sync