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:
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:
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
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.