Building a Local SMS Sync Bridge: Integrating macOS Messages with Android via ADB
Over the past development session, I tackled a challenging problem: creating a reliable SMS synchronization pipeline that bridges macOS Messages.app with Android devices without relying on third-party SMS services like Twilio. This post details the technical architecture, implementation decisions, and the tooling I built to make it work.
The Problem
The existing infrastructure was tightly coupled to Twilio for SMS operations, which introduced unnecessary complexity and cost for a use case that primarily needed to read and sync SMS from a local Android device. The goal was to:
- Read SMS messages directly from an Android device
- Sync them into macOS Messages.app for a unified inbox
- Generate digests from message threads without external service dependencies
- Maintain a daemon process that runs continuously on the development machine
Technical Architecture
Core Components
I created two primary Python modules in /Users/cb/Documents/repos/tools/:
samsung_sms_sync.py– The main synchronization daemon that handles message pulling and local storagesamsung_sms_auth.py– Authentication and device credential management for ADB connections
The architecture follows a classic pull-based pattern: the daemon periodically queries the Android device's SMS database via Android Debug Bridge (ADB), parses the resulting message records, and stores them in a format consumable by downstream processes.
ADB Integration
Rather than implementing a custom USB protocol handler, I leveraged Android Debug Bridge, which ships with the Android SDK platform tools. Installation was straightforward:
brew install android-platform-tools
On Apple Silicon Macs, adb installs to /opt/homebrew/bin/adb. The tool provides a shell interface to the Android device once enabled in developer settings, allowing direct SQLite queries against the device's SMS provider database:
adb shell content query --uri content://sms/ --projection address,date,body,type
This query returns SMS records with sender address, timestamp, message body, and message type (incoming vs. outgoing). The output is pipe-delimited and human-readable, making parsing trivial with Python's standard library.
Message Persistence Strategy
Rather than pushing messages back into Twilio or a centralized database, I chose to write them into the local macOS Messages database, leveraging the existing Messages framework available via Python's osascript bridge. This approach:
- Keeps data local and private
- Integrates seamlessly with the native Messages.app UI
- Eliminates the need for additional data stores
- Allows standard macOS search and archival tools to work on SMS data
The sync process identifies existing conversations by phone number and appends new messages, avoiding duplicates through timestamp-based deduplication.
LaunchAgent Daemon Setup
To run the sync continuously, I created a macOS LaunchAgent plist at /Users/cb/Library/LaunchAgents/com.cb.samsung-sms-sync.plist. The plist defines:
- The Python script path
- Interval-based execution (default: every 60 seconds)
- Log file paths for debugging
- Restart policy on failure
The LaunchAgent runs in user context (not system-wide), which is appropriate since ADB device access requires the current user's authentication. This also avoids privilege escalation and keeps the daemon tightly scoped.
To load it:
launchctl load ~/Library/LaunchAgents/com.cb.samsung-sms-sync.plist
Authentication and Device Discovery
The samsung_sms_auth.py module handles device enumeration and credential caching. On first run, it:
- Queries connected ADB devices via
adb devices - Stores device serial numbers and connection metadata
- Implements exponential backoff if a device becomes unreachable
- Logs authentication attempts for troubleshooting
This separation of concerns keeps the authentication logic testable and reusable across different sync scenarios.
Data Flow and Processing
The sync pipeline follows this sequence:
- Device Query: ADB shell executes a content provider query against the SMS database
- Parsing: Output is split by row and parsed into structured message objects
- Deduplication: Messages are checked against a local cache (keyed by phone + timestamp + body hash)
- Transformation: Message metadata is normalized for macOS Messages.app compatibility
- Insertion: Messages are inserted into Messages.app via the AppleScript bridge or direct database write
- Digest Generation: Conversation threads are extracted and formatted for email delivery via SES
Digest Email Generation
Once messages are synced, downstream processes can generate conversation digests. The existing infrastructure already had SES configured for email delivery. The digest generator:
- Groups messages by sender phone number
- Orders by timestamp descending
- Extracts recent activity (last 48 hours or last N messages)
- Formats as plain text with conversation headers
- Sends via SES to configured recipients
This allows near real-time awareness of incoming SMS without opening the Messages app.
Key Decisions and Tradeoffs
Why ADB Instead of Third-Party Bridges?
Tools like KDE Connect and Android SMS Bridge apps add extra infrastructure and require internet connectivity for some operations. ADB is built into Android, requires only USB (or Wi-Fi in developer mode), and is battle-tested in production mobile development environments. The tradeoff is that the Android device must have developer options enabled, but this is a one-time setup cost.
Why Local Sync Instead of Cloud?
Pushing SMS to a cloud database introduces latency, dependency on internet connectivity, and additional operational overhead. Since the development machine is always on (or can be), local-first sync is simpler and more reliable for this use case. The Messages.app database provides free, queryable storage with built-in UI.
Why Interval-Based Rather Than Event-Based?
ADB doesn't support server-mode push notifications for new SMS. Interval-based polling (every 60 seconds) is simple, predictable, and sufficient for most use cases. If sub-second latency becomes critical, the system can be refactored to use Android's content://sms/inbox observer pattern, but the added complexity isn't justified yet.
File Structure Reference
/Users/cb/Documents/repos/tools/
├── samsung_sms_sync.py # Main daemon
├── samsung_sms_auth.py # Auth and device management
└── (other tool scripts)
/Users/cb/Library/LaunchAgents/
└── com.cb.samsung-sms-sync.plist # Daemon registration