Local-only iMessage scheduler for macOS.
- Web app schedules messages,
- Gateway sends via AppleScript
- Worker updates delivery status by reading
~/Library/Messages/chat.db.
This project was built to practice systems design under hard constraints, not to build another CRUD app.
Because iMessage has no public API or delivery webhooks, the system is designed around time, state, and ownership of execution. A gateway worker polls, enforces FIFO ordering, applies locking for idempotency, and infers delivery status from local data instead of relying on events.
The goal is to demonstrate reliable workflow design, polling vs event-driven tradeoffs, and stateful systems consideration when standard APIs don’t exist.
# Use Node 22.x (see .nvmrc)
# installs both dependencies web & gateway
pnpm run setupbrew install mysql
brew services start mysql
mysql -uroot -e "CREATE DATABASE imessage_scheduler;"Create .env in repo root:
DATABASE_URL=mysql://root@localhost:3306/imessage_scheduler
GATEWAY_SECRET=dev-secret
GATEWAY_PORT=4001
WEB_PORT=3000
WEB_BASE_URL=http://localhost:3000If your local MySQL user has a password:
DATABASE_URL=mysql://root:<password>@localhost:3306/imessage_schedulerpnpm db:generate
pnpm db:migrate
pnpm db:seed# starts gateway & web
pnpm run devThe gateway reads Messages.app data from ~/Library/Messages/chat.db.
On macOS:
- System Settings → Privacy & Security → Full Disk Access
- Add your terminal app (Terminal / iTerm) or your IDE (VS Code) and enable it
- Restart the terminal/IDE, then re-run the gateway
Seeded users:
- user1@example.com (free) / password123
- user2@example.com (paid) / password123
- Local-only: runs on a single macOS machine, no cloud dependencies.
- Recipients: US phone numbers only (normalized to E.164).
- Timeline: single-day view, 30‑minute slots, drag-to-create/move, edit/cancel, duplicate.
- Gateway: AppleScript send, FIFO worker, per-user rate limiting.
- Receipts: best-effort correlation with chat.db and polling for DELIVERED/RECEIVED.
- UX: Default scrolling to current time on today's date. Duplicate button for easy creation of mutiple schedules.
Rate limits:
- FREE: 0s min interval, 2 per hour
- PAID: 0s min interval, 30 per hour
- Web app schedules a message (status = QUEUED).
- Gateway worker picks next eligible message FIFO, locks it, sends via AppleScript.
- Web app receives SENT callback and stores receipt correlation metadata.
- Gateway polls chat.db for delivery/read indicators (receipt) and posts DELIVERED/RECEIVED when detected.
schedule → queued → worker → status callbacks → receipts
- Next.js (App Router)
- MySQL + Drizzle ORM
- Tailwind + shadcn/ui
app/Next.js app + API routesgateway/Node gateway + worker + receipt pollingpackages/shared/shared types, schemas, helpersdocs/Source of Truth & implementation plansapp/lib/dbdb logic including .models for shared db queries