Outbox Architecture: Zero Message Loss Design
February 13, 2026
The Challenge: Instant UI Clear Without Losing Messages
In Simple Memo (Captio-style), the UI clears the instant you tap send, letting you immediately start writing your next memo. This is the core of the Captio-style UX.
But real-world networks have 300ms-2s latency. They fail. They go offline entirely. Most apps wait for a network response before clearing the UI. Safe, but terrible UX. Clear immediately? You risk losing the message.
The Outbox pattern solves both simultaneously. The moment you tap send, the message is persisted to encrypted local storage (the Outbox), the UI clears instantly, and background network delivery begins. On success, the message is removed from the Outbox. On failure, automatic retry kicks in. The result: zero message loss with near-zero perceived latency.
Outbox Pattern Design Flow
Here is the complete flow from tapping send to guaranteed message delivery.
- Tap Send → Persist to Outbox with AES-GCM Encryption
The instant the user taps send, the message is encrypted with AES-GCM 256-bit and persisted to local storage. The UI does not clear until this step completes. Outbox persistence is the first line of defense for zero message loss. - Clear UI Immediately (User Can Start Writing Next Memo)
Once Outbox persistence succeeds, the text view clears and the send animation plays. From the user's perspective, the message has been "sent." No network response has been waited for. Send-to-Reset target: one-tap (0.25s with animation). - Background Send via Relay API
In parallel with the UI clear, SendManager fires an HTTP request to the Relay API on Cloudflare Workers. This is fully asynchronous and never blocks the main thread. - On Success: Remove from Outbox
When the Relay API returns a 200 response, the message is safely deleted from the Outbox. The message lifecycle is complete. - On Failure: Automatic Retry with Exponential Backoff
For network or server errors, automatic retry with exponential backoff (1s → 2s → 4s → 8s...) kicks in. BGTaskScheduler ensures retries happen even when the app is in the background. - If Offline: NWPathMonitor Detects Connectivity and Auto-Resends
If the device is offline at send time, Network.framework's NWPathMonitor watches for connectivity changes. The moment Wi-Fi or cellular returns, all unsent messages in the Outbox are automatically resent.
SendManager Code
The SendManager's send() function is the entry point for the Outbox pattern. It persists to the Outbox first, queues if offline, and sends immediately if connected.
func send(message: String, completion: @escaping (SendResult) -> Void) {
// Persist to Outbox first (guarantees zero message loss)
let outboxMessage: OutboxMessage
do {
outboxMessage = try OutboxManager.shared.add(body: message)
} catch {
DispatchQueue.main.async { completion(.failure(error)) }
return
}
// If no network, notify that message has been queued
guard NetworkMonitor.shared.isConnected else {
BackgroundTaskManager.shared.scheduleRetryTask()
DispatchQueue.main.async { completion(.queued) }
return
}
// Execute the send
performSend(message: message, outboxId: outboxMessage.id, ...)
}
The critical point: OutboxManager.shared.add(body:) writes synchronously to persistent storage. Once this call succeeds, message safety is guaranteed. Regardless of what happens with the network send, the message remains in the Outbox until explicitly deleted after confirmed delivery.
AES-GCM Encryption Implementation
Messages stored in the Outbox are private user memos. Even on local storage, plaintext is unacceptable. We implemented encryption using Apple CryptoKit's AES-GCM, with no third-party SDK in the encryption path.
- Apple CryptoKit — Available from iOS 13+. No external dependencies.
- 256-bit Symmetric Key — Stored in Keychain, bound to the device.
- Authenticated Encryption — AES-GCM guarantees both confidentiality and integrity. Tampered data is detected on decryption.
enum OutboxEncryption {
private static var encryptionKey: SymmetricKey {
if let existingKey = KeychainHelper.load(key: "outbox_encryption_key") {
return SymmetricKey(data: existingKey)
}
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
KeychainHelper.save(key: "outbox_encryption_key", data: keyData)
return newKey
}
static func encrypt(_ plainText: String) throws -> Data {
guard let data = plainText.data(using: .utf8) else {
throw OutboxError.encryptionFailed
}
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
guard let combined = sealedBox.combined else {
throw OutboxError.encryptionFailed
}
return combined
}
}
The key is auto-generated on first launch and securely stored in Keychain. Keychain is tied to the device's Secure Enclave, making it inaccessible to other apps or backups.
Keeping the Data Path Free of External SDKs
Every framework used in the Outbox architecture is Apple-native.
- CryptoKit — AES-GCM encryption. No third-party crypto libraries needed.
- Network.framework — NWPathMonitor for connectivity monitoring. No Reachability library needed.
- BackgroundTasks — BGTaskScheduler for background retry.
Zero external dependencies has significant implications:
- No Breaking Changes — Third-party libraries won't break on OS updates.
- Security — No third-party code touches the user's memo data. The entire code path handling user content is first-party.
- Small Binary — No unnecessary frameworks means minimal download size.
- Faster Builds — No dependency resolution overhead. CI stays fast.
Decoupling UI Clear from Network Send
The true value of the Outbox pattern is the complete separation of the UI layer and the network layer. Let's look at the send button tap handler.
@objc private func sendButtonTapped() {
PerformanceLogger.shared.beginSendToReset()
let message = textView.text ?? ""
isSending = true
sendBarButton.isEnabled = false
// Animation → UI clear (does NOT wait for network!)
performSendAnimation {
self.clearTextView()
PerformanceLogger.shared.endSendToReset()
}
// Send is async. Does not block UI.
SendManager.shared.send(message: message) { [weak self] result in
self?.isSending = false
self?.updateSendButtonState()
switch result {
case .success:
PerformanceLogger.shared.logEvent(name: "SendSuccess")
case .failure(let error):
self?.handleSendError(error)
case .queued:
self?.showQueuedFeedback()
}
}
}
performSendAnimation and SendManager.shared.send execute completely independently. The animation completes in 0.25 seconds and the text view clears. The network send proceeds asynchronously in the background, never blocking the UI.
Send-to-Reset target: one-tap. Even with animation, 0.25 seconds. The user can start writing their next memo immediately after tapping send. This is the Captio-style UX.
FAQ
Q. What happens to memos sent offline?
They are encrypted and stored in the Outbox. When connectivity returns, NWPathMonitor detects it and automatically resends all pending messages. No user action required.
Q. Are memos lost if I close the app?
No. The Outbox uses persistent storage. Messages survive app termination and iPhone restarts. They are automatically retried on next app launch or via background tasks.
Q. Why AES-GCM?
Apple CryptoKit provides native AES-GCM support, offering authenticated encryption that detects data tampering during decryption. The encryption code path is entirely first-party — no third-party SDK touches the security-critical code.
Q. How many retries on failure?
Retries use exponential backoff and are scheduled as background tasks via BGTaskScheduler. They continue until the message is successfully delivered. The system is designed to never drop a user's memo.
Related Articles
References
- Apple Developer — AES.GCM (CryptoKit) — Official reference for AES-GCM authenticated encryption used to protect Outbox data
- Apple Developer — NWPathMonitor (Network Framework) — Used for offline detection and network status monitoring
- Apple Developer — BGTaskScheduler — Used for scheduling background retry processing
- Wikipedia — Transactional Outbox Pattern — Explanation of the transactional Outbox pattern for reliable message delivery