https://github.com/photon-hq/vercel-chat-adapter-imessage
iMessage adapter for Chat SDK. Supports both local (on-device) and Photon iMessage integration.
https://github.com/photon-hq/vercel-chat-adapter-imessage
agent ai chat chat-sdk imessage vercel
Last synced: 3 months ago
JSON representation
iMessage adapter for Chat SDK. Supports both local (on-device) and Photon iMessage integration.
- Host: GitHub
- URL: https://github.com/photon-hq/vercel-chat-adapter-imessage
- Owner: photon-hq
- License: mit
- Created: 2026-03-01T18:30:58.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-03-06T06:46:47.000Z (3 months ago)
- Last Synced: 2026-03-06T14:34:08.120Z (3 months ago)
- Topics: agent, ai, chat, chat-sdk, imessage, vercel
- Language: TypeScript
- Homepage:
- Size: 67.4 KB
- Stars: 7
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# chat-adapter-imessage
iMessage community adapter for [Chat SDK](https://chat-sdk.dev/docs). Supports both local (on-device) and remote ([Photon](https://photon.codes)-based) iMessage integration.
## Installation
```bash
pnpm add chat chat-adapter-imessage
```
## Usage
The adapter supports two modes: **local** (running directly on a Mac with iMessage) and **remote** (connecting to a [Photon](https://photon.codes) iMessage server). The mode is auto-detected from the `IMESSAGE_LOCAL` environment variable.
### Remote mode
Recommended for production. Connects to [Photon](https://photon.codes)'s managed iMessage service over HTTP and Socket.IO, so your bot can run on any platform.
```typescript
import { Chat } from "chat";
import { createiMessageAdapter } from "chat-adapter-imessage";
const bot = new Chat({
userName: "mybot",
adapters: {
imessage: createiMessageAdapter({
local: false,
}),
},
});
bot.onNewMention(async (thread, message) => {
await thread.post("Hello from iMessage!");
});
```
### Local mode
For development or self-hosted deployments using [imessage-kit](https://github.com/photon-hq/imessage-kit). Reads from the local iMessage database and sends via AppleScript. Must run on macOS with **Full Disk Access** granted.
```typescript
import { Chat } from "chat";
import { createiMessageAdapter } from "chat-adapter-imessage";
const bot = new Chat({
userName: "mybot",
adapters: {
imessage: createiMessageAdapter({
local: true,
}),
},
});
bot.onNewMention(async (thread, message) => {
await thread.post("Hello from iMessage!");
});
```
## Setup
### Remote mode
Remote mode connects to a [Photon](https://photon.codes) iMessage server, which handles the macOS-side integration on your behalf. You'll need an active Photon subscription to get your server credentials.
1. [Request access](https://photon.codes) from Photon to get your server credentials
2. Copy your **server URL** and **API key** from the Photon dashboard
3. Set `IMESSAGE_SERVER_URL` and `IMESSAGE_API_KEY` environment variables
4. Set `IMESSAGE_LOCAL=false`
### Local mode
Local mode requires the adapter to run directly on a macOS machine with iMessage. It uses Apple's native APIs — reading from the local `chat.db` database and sending messages via AppleScript — with no external server required.
1. Grant **Full Disk Access** to your terminal or application in **System Settings > Privacy & Security > Full Disk Access**
2. Ensure iMessage is signed in and working on the Mac
3. No additional environment variables are required — local mode is the default
## Receiving messages
Call `startGatewayListener()` to listen for new messages in real-time. In remote mode, this uses Socket.IO push events. In local mode, it polls the iMessage database.
In serverless environments, use a cron job to maintain the connection.
## Gateway setup for serverless
### 1. Create Gateway route
```typescript
// app/api/imessage/gateway/route.ts
import { after } from "next/server";
import { bot } from "@/lib/bot";
export const maxDuration = 800;
export async function GET(request: Request): Promise {
const cronSecret = process.env.CRON_SECRET;
if (!cronSecret) {
return new Response("CRON_SECRET not configured", { status: 500 });
}
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${cronSecret}`) {
return new Response("Unauthorized", { status: 401 });
}
const durationMs = 600 * 1000;
return bot.adapters.imessage.startGatewayListener(
{ waitUntil: (task) => after(() => task) },
durationMs
);
}
```
### 2. Configure Vercel Cron
```json
// vercel.json
{
"crons": [
{
"path": "/api/imessage/gateway",
"schedule": "*/9 * * * *"
}
]
}
```
This runs every 9 minutes, ensuring overlap with the 10-minute listener duration.
### 3. Environment variables
`CRON_SECRET` is automatically added by Vercel when you configure cron jobs.
## Configuration
| Option | Required | Description |
|--------|----------|-------------|
| `local` | No | `true` for local mode, `false` for remote. Auto-detected from `IMESSAGE_LOCAL` (default: `true`) |
| `serverUrl` | Remote only | URL of the remote iMessage server. Auto-detected from `IMESSAGE_SERVER_URL` |
| `apiKey` | Remote only | API key for remote server authentication. Auto-detected from `IMESSAGE_API_KEY` |
| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
## Environment variables
```bash
# .env.local
IMESSAGE_LOCAL=false # Set to "false" for remote mode (default: true)
IMESSAGE_SERVER_URL=https://... # Required for remote mode
IMESSAGE_API_KEY=... # Required for remote mode
```
## Features
| Feature | Supported |
|---------|-----------|
| Mentions | DMs only |
| Reactions (add/remove) | Remote only |
| Modals | Limited (Remote only) |
| Cards | No |
| Streaming | No |
| DMs | Yes |
| Ephemeral messages | No |
| File uploads | Yes |
| Typing indicator | Remote only |
| Message history | Yes |
| Message editing | Remote only |
## Modals (Limited)
Remote mode supports limited modal functionality by mapping the Chat SDK's `openModal()` to iMessage native polls via the Photon SDK. Only `Select` children are supported — the first `Select` in the modal is used to create a poll.
- `Modal.title` becomes the poll question
- `Select.options` become the poll choices
- Votes trigger `onModalSubmit` with the selected option's `value`
```typescript
import { Chat, Modal, Select, SelectOption } from "chat";
import { createiMessageAdapter } from "chat-adapter-imessage";
const bot = new Chat({
userName: "mybot",
adapters: {
imessage: createiMessageAdapter({ local: false }),
},
});
bot.onNewMention(async (thread, message) => {
await message.openModal(
Modal({
callbackId: "fav-color",
title: "What is your favorite color?",
children: [
Select({
id: "color",
label: "Pick a color",
options: [
SelectOption({ label: "Red", value: "red" }),
SelectOption({ label: "Blue", value: "blue" }),
SelectOption({ label: "Green", value: "green" }),
],
}),
],
})
);
});
bot.onModalSubmit("fav-color", async (event) => {
const color = event.values.color;
// color will be "red", "blue", or "green"
});
```
**Not supported:**
- `Select.placeholder` and `Select.label` — iMessage polls don't have these fields
- `TextInput`, `RadioSelect`, and other modal children — silently ignored
- `Modal.submitLabel` and `Modal.closeLabel` — not applicable to polls
- Only the **first** `Select` child is used; other children are ignored
- Local mode — `openModal()` throws `NotImplementedError`
## Tapback reactions
iMessage uses tapbacks instead of emoji reactions. The adapter maps standard emoji names to iMessage tapbacks:
| Emoji | Tapback |
|-------|---------|
| `love` / `heart` | Love |
| `like` / `thumbs_up` | Like |
| `dislike` / `thumbs_down` | Dislike |
| `laugh` | Laugh |
| `emphasize` / `exclamation` | Emphasize |
| `question` | Question |
## Limitations
- **Local mode**: Only supports sending/receiving messages, message history, and file uploads. Reactions, typing indicators, message editing, modals, and thread fetching require remote mode.
- **Formatting**: iMessage is plain-text only. Markdown formatting (bold, italic, etc.) is stripped when sending messages, preserving only the text content.
- **Platform**: Local mode requires macOS. Remote mode can run on any platform — [Photon](https://photon.codes) manages the iMessage infrastructure for you.
- **Cards**: iMessage has no support for structured card layouts.
- **Modals**: Limited to `Select`-based modals mapped to iMessage native polls. Only the first `Select` child is used; `placeholder`, `label`, `TextInput`, `RadioSelect`, and other fields are not supported. Remote mode only.
## Troubleshooting
### "serverUrl is required" error
- Set `IMESSAGE_SERVER_URL` or pass `serverUrl` in config when using remote mode
- This error occurs when `IMESSAGE_LOCAL=false` but no server URL is provided
### "apiKey is required" error
- Set `IMESSAGE_API_KEY` or pass `apiKey` in config when using remote mode
### Local mode not receiving messages
- Verify **Full Disk Access** is granted to your terminal or application
- Check that iMessage is signed in and working
- Messages are polled from the local database — there may be a short delay
### Remote mode connection issues
- Verify the server URL is correct and accessible
- Check that the API key matches your Photon iMessage service credentials
- Confirm your Photon subscription is active
## License
MIT