Architecture Overview
AiMessage is organized into three layers. Each layer has a single responsibility, and dependencies only flow downward.
Layer diagram
┌─────────────────────────────────────────────────┐
│ API Layer (Axum) │
│ HTTP endpoints · Auth middleware · Request DTOs │
│ WebSocket handler · Route definitions │
└────────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Core Layer │
│ MessageBackend trait · Domain types │
│ Webhook dispatcher with retry │
│ Broadcast channel (event bus) │
└────────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ iMessage Layer │
│ chat.db reader (ROWID polling) │
│ AppleScript sender (osascript) │
│ MessageBackend implementation │
└─────────────────────────────────────────────────┘
Data flow
Inbound (receiving messages)
- The iMessage layer polls
chat.dbeverypoll_interval_msmilliseconds, comparing the current max ROWID against the last processed ROWID. - New rows are read, parsed into
MessageorReactiondomain types, and wrapped inEventvariants. - Events are published to a
tokio::sync::broadcastchannel. - Two subscribers consume from that channel simultaneously:
- The webhook dispatcher (in the core layer) fans out to all registered webhook URLs with retry logic.
- Each connected WebSocket client receives the event as a JSON text frame.
Outbound (sending messages)
- An API request hits
POST /api/v1/messages. - The handler calls the
MessageBackendtrait methodsend_message. - The iMessage layer implementation invokes
osascriptwith the recipient and body. - Messages.app sends the message. The sent message eventually appears in
chat.dband is picked up by the polling loop as amessage.sentevent.
Broadcast channel
The broadcast channel is the central event bus. It decouples the iMessage poller from all consumers. Key properties:
- Multiple consumers (webhooks, WebSocket clients) subscribe independently.
- The channel has a fixed capacity. Lagged consumers (WebSocket clients that are too slow) have events skipped rather than the channel blocking.
- The webhook dispatcher is a single task that reads from the channel and fans out to all registered URLs concurrently.
Storage
AiMessage uses two SQLite databases:
| Database | Location | Contents |
|---|---|---|
chat.db | ~/Library/Messages/chat.db | iMessage data — read-only |
aimessage.db | ~/.aimessage/aimessage.db | App state: registered webhooks, last processed ROWID |