Platform & Server

Notifications & Background Modes

Foreground services, periodic WorkManager task schedulers, signature-polling status checks, and flat-file inbox buffering.

Overview

Because Wiltkey does not use a central notification server (such as Firebase Cloud Messaging), background message detection is handled directly by client isolates. Devices choose between two background execution models depending on battery and performance preferences: Instant Mode (foreground WebSocket connection) or Low Power Mode (periodic HTTP polling).

Background Sync Architectures

1. Instant Mode (Foreground Service)

Instant Mode spawns a persistent background isolate (_MessageTaskHandler) using flutter_foreground_task. It maintains an active, authenticated WebSocket connection to the relay, streaming messages as they arrive.

Secure Flat-File Buffering: When a message arrives while the device is locked, the master key is not in RAM, meaning the SQLite database cannot be decrypted or updated. To prevent dropping messages (the relay deletes messages once delivered), the background isolate appends the raw, encrypted envelope to a flat, append-only JSON lines file:

DocumentsDirectory/wiltkey_pending_inbox.jsonl
If the content type is a user message (text, image, group_message), a local notification is fired. When the user unlocks the app, the main isolate reads this file, decrypts all buffered envelopes, writes them to SQLite, and deletes the file.

2. Low Power Mode (Periodic Polling)

Low Power Mode utilizes Android's WorkManager to run a periodic background task (callbackDispatcher) approximately every 10 minutes. Rather than establishing a full WebSocket session or downloading messages (which would violate privacy and drain battery), it calls a signature-authenticated endpoint on the server:

lib/core/notifications/background_handler.dart DART
Future<bool> _pollQueueStatus(_BgCreds creds) async {
  final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString();
  // Sign identity with Ed25519 key stashed in secure storage
  final sig = _sign(creds.privKeyHex, '${creds.userId}:$ts');
  final response = await client.get("/api/v1/queue/status?id=${creds.userId}×tamp=$ts&sig=$sig");
  return response.json['has_payload'] == true;
}

If has_payload is true, a local notification is shown. Messages are not downloaded until the user opens the application, ensuring decryption keys remain locked.

Key Files & Symbols

File Path Symbol Name Description
lib/core/notifications/background_handler.dart startCallback() Entry point for the Foreground Service background isolate.
lib/core/notifications/background_handler.dart callbackDispatcher() Entry point for the WorkManager background polling dispatcher.
lib/core/notifications/pending_inbox.dart PendingInbox Manages appends, reads, and clears of the raw JSON lines buffer wiltkey_pending_inbox.jsonl.
lib/core/state_inbound.dart processPendingInbox() Processes and decrypts the flat-file buffer on application unlock.

Gotchas & Edge Cases

🛑 DATABASE WRITE LOCK CONTENTION
SQLite does not allow simultaneous write operations from separate OS processes or isolates. Writing to SQLite from a background isolate while the main isolate is running will cause write lock collisions. For this reason, PendingInbox uses a simple flat-file write rather than the SQLite database.