Content Transfer
16. Push Notification Integration
Section titled “16. Push Notification Integration”⚠️ Critical for Mobile: Push notifications are not optional for production mobile apps. Without them, users must manually open the app to receive synced data.
16.1 Architecture
Section titled “16.1 Architecture”16.2 Client API (Day 1 Requirements)
Section titled “16.2 Client API (Day 1 Requirements)”The sync-client MUST expose push token hooks from Day 1, even if backend integration comes later:
// sync-client API
impl SyncClient { pub async fn register_push_token( &self, token: String, platform: PushPlatform, ) -> Result<()>;
pub async fn unregister_push_token(&self) -> Result<()>;}
pub enum PushPlatform { Apns, // Apple Push Notification Service Fcm, // Firebase Cloud Messaging Web, // Web Push (future)}// JavaScript/TypeScript bindings (for JS-based frameworks)export interface SyncClient { // ... existing methods ...
// Push notification integration (Day 1) registerPushToken(token: string, platform: 'apns' | 'fcm'): Promise<void>; unregisterPushToken(): Promise<void>;
// Event for handling push-triggered wake on(event: 'push-wake', handler: () => void): void;}16.3 Message Specification
Section titled “16.3 Message Specification”REGISTER_PUSH
Section titled “REGISTER_PUSH”pub struct RegisterPush { /// Platform-specific push token pub token: String,
/// Platform identifier pub platform: PushPlatform,
/// App bundle ID (for APNS) pub app_id: Option<String>,}UNREGISTER_PUSH
Section titled “UNREGISTER_PUSH”pub struct UnregisterPush { /// Reason for unregistering pub reason: UnregisterReason,}
pub enum UnregisterReason { UserDisabled, TokenExpired, AppUninstalled,}16.4 Relay Push Behavior
Section titled “16.4 Relay Push Behavior”On PUSH (when recipient offline):
async fn handle_push_for_offline_device( device: &Device, blob: &Blob, push_service: &PushService,) { if let Some(push_token) = device.push_token { // Send silent push notification let notification = PushNotification { token: push_token, platform: device.push_platform, payload: PushPayload::SilentSync { group_id: blob.group_id, cursor: blob.cursor, }, // Silent push - no user-visible notification content_available: true, alert: None, };
push_service.send(notification).await?; }}16.5 iOS/Android Integration
Section titled “16.5 iOS/Android Integration”iOS (APNS):
// In your iOS app delegatefunc application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.hexString // Pass to your sync client (adapt to your framework's native bridge) SyncClient.shared.registerPushToken(token: token, platform: .apns)}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // Silent push received - trigger sync SyncClient.shared.handlePushWake() fetchCompletionHandler(.newData)}Android (FCM):
// In your Firebase messaging serviceclass SyncFirebaseService : FirebaseMessagingService() { override fun onNewToken(token: String) { // Pass to your sync client (adapt to your framework's native bridge) SyncClient.getInstance().registerPushToken(token, PushPlatform.FCM) }
override fun onMessageReceived(message: RemoteMessage) { if (message.data["type"] == "sync") { // Trigger sync in background SyncClient.getInstance().handlePushWake() } }}16.6 Implementation Phases
Section titled “16.6 Implementation Phases”| Phase | Scope | Dependency |
|---|---|---|
| Day 1 | Client API hooks (register/unregister) | None |
| Day 1 | Message types (REGISTER_PUSH, etc.) | sync-types |
| Phase 5 | Relay stores push tokens | sync-relay |
| Phase 5 | Relay sends to APNS/FCM | Push service integration |
| Future | Web Push support | Service workers |
Key Point: The client-side hooks MUST exist from Day 1 so apps can register tokens. The relay-side push sending can come later, but the API contract must be stable.
17. Large Content Transfer Protocol
Section titled “17. Large Content Transfer Protocol”Amendment (2026-02-02): Added per iroh-deep-dive-report.md recommendations.
17.1 The Problem
Section titled “17.1 The Problem”The sync protocol is optimized for small encrypted blobs (app state, JSON entries, ~100 KB sweet spot). When apps need to sync photos (2-10 MB), voice memos (1-5 MB), document scans (0.5-3 MB), or PDFs, pushing megabytes through a relay designed for kilobytes is inefficient.
Affected use cases by app type:
| App Type | Content | Size Range | Frequency |
|---|---|---|---|
| Journal apps | Photos, voice memos | 1-15 MB | High (daily) |
| Health apps | Meal photos, progress pics, scans | 1-10 MB | High |
| Finance apps | Receipt scans, invoice PDFs | 0.5-5 MB | Medium |
| Note apps | Article images, PDF attachments | 0.5-20 MB | Medium |
| Contact apps | Contact photos | 50-500 KB | Low (small enough for sync relay) |
| Password managers | None | — | None |
17.2 Architecture: Encrypt-Then-Hash with iroh-blobs
Section titled “17.2 Architecture: Encrypt-Then-Hash with iroh-blobs”Large content bypasses the sync relay entirely. The relay handles only small metadata; actual content transfers device-to-device via iroh-blobs.
Flow:
Key properties:
- iroh-blobs sees only ciphertext — BLAKE3 hash is of encrypted bytes
- Resumable — iroh-blobs resumes from last verified chunk if interrupted
- Verified — every 16 KiB chunk integrity-checked during streaming
- No relay load — large files travel P2P, relay handles only metadata
17.3 ContentReference Message
Section titled “17.3 ContentReference Message”See Wire Protocol — Section 5.3 for the full struct definition. Key fields:
| Field | Purpose |
|---|---|
content_hash | BLAKE3 hash of ciphertext (iroh-blobs address) |
encryption_nonce | XChaCha20-Poly1305 nonce for decryption |
content_size | Original size (for UI progress) |
mime_type | Content type for app handling |
thumbnail_hash | Optional preview (encrypted, much smaller) |
17.4 Content Key Derivation
Section titled “17.4 Content Key Derivation”See Security Model — Section 4.1 for the HKDF derivation. Same key for all devices in the group.
17.5 Garbage Collection
Section titled “17.5 Garbage Collection”| Device | GC Strategy |
|---|---|
| Sender | Keeps blob tagged until all devices ACK |
| Receiver | Drops tag after successful download + local storage |
| Orphan protection | Sender keeps tag for grace period (30 days) if ContentRef deleted before all fetches |
17.6 Mobile Considerations
Section titled “17.6 Mobile Considerations”Thumbnail-first strategy:
- Show “Photo attached” immediately (from ContentRef)
- Show thumbnail if
thumbnail_hashpresent (tiny iroh-blobs fetch) - Download full resolution on demand or WiFi
Background transfer:
- Queue large downloads for WiFi / charging
- iroh-blobs handles resumption natively
- iOS/Android background fetch can trigger thumbnail downloads
Storage quota:
- Each app manages its own content storage budget
- Old content can be evicted and re-fetched (ContentRef persists in sync log)
17.7 Offline / Asynchronous Transfer
Section titled “17.7 Offline / Asynchronous Transfer”| Scenario | Solution |
|---|---|
| Same LAN | mDNS direct discovery → LAN-speed transfer |
| Different networks, both online | iroh relay forwards encrypted blobs |
| Asynchronous (rarely online together) | Pending downloads queue, resume on next app foreground |
| Future enhancement | Relay-hosted blob cache with TTL (deployment optimization) |
17.8 Discovery Configuration
Section titled “17.8 Discovery Configuration”// Enable mDNS for same-LAN direct transfer (recommended)let discovery = ConcurrentDiscovery::new() .add(MdnsDiscovery::new()) // LAN discovery .add(DnsDiscovery::new(dns_url)) // Our DNS server .add(DhtDiscovery::new()); // Fallback (optional)
let endpoint = Endpoint::builder() .discovery(discovery) .relay_mode(RelayMode::Custom(our_relay_map)) // Our infrastructure only .build() .await?;17.9 Self-Hosted Infrastructure
Section titled “17.9 Self-Hosted Infrastructure”For production deployments, run your own iroh infrastructure:
| Component | Purpose | Deployment |
|---|---|---|
iroh-relay | Encrypted datagram relay when P2P fails | Docker, any VPS |
iroh-dns-server | Endpoint discovery by ID | Docker, your domain |
| mDNS | LAN discovery | Zero infrastructure |
| DHT | Decentralized fallback | Zero infrastructure |
Configuration:
// Disable n0 default relays, use only our infrastructurelet relay_map = RelayMap::from_url("https://relay.yourdomain.com".parse()?);This ensures zero runtime dependency on any third party.