iMessage Webhook Handler (BlueBubbles)
Internal webhook receiver for BlueBubbles iMessage events from Aurora’s Mac Mini. Receives real-time message events over the Tailscale network, indexes them into Supabase via the BlueBubbles indexer, and fires the Aurora iMessage responder as a fire-and-forget child process. Never exposed to the public internet.
Endpoint
| Path | Source | Network |
|---|---|---|
POST / (or configured path) | BlueBubbles v1.9.9 on auroras-mac-mini | Tailscale only |
Port: 18802 (or process.env.PORT)
Source Tailscale IP: 100.75.34.91 (auroras-mac-mini)
Public exposure: None — internal Tailscale peer traffic only. Not in Cloudflare Tunnel.
Auth method
Query-parameter token — ?token=<BLUEBUBBLES_WEBHOOK_SECRET>. Constant-time comparison via crypto.timingSafeEqual(). BlueBubbles does NOT support HMAC payload signing — this is by design per the BlueBubbles API (source: knowledge-base/bluebubbles/WEBHOOKS.md).
All traffic arrives from Tailscale peer 100.75.34.91. Network-layer isolation provides additional defense-in-depth.
Payload shape
BlueBubbles sends a standard event envelope:
{
"type": "<event-type>",
"data": <object|string>
}Important edge case: The "hello-world" test event sends data as a string, not an object. All other event types send data as an object. The handler must check typeof data before accessing fields.
| Event type | Description |
|---|---|
new-message | New iMessage received |
updated-message | Message read receipt or status update |
chat-read-status-changed | Chat marked read |
hello-world | Connectivity test (data = string) |
Dedup key: data.guid (primary) — fallback: sha256(rawBody) when guid absent.
Downstream dispatch
imessage-handler.js
├─ checkDedup(data.guid) → webhook-dedup.js (24-hr TTL)
├─ auditLog() → webhook-audit.js
├─ recordBluebubblesMessage() → bluebubbles-indexer.js → Supabase
├─ recordBluebubblesMessageUpdate() → bluebubbles-indexer.js
├─ recordOmniEvent() / recordOmniEventForChatAction() → bluebubbles-indexer.js
└─ Aurora iMessage Runner (fire-and-forget, non-blocking)
→ agents/aurora/agent/skills/imessage-responder/runner.js
→ spawns Claude CLI child process
Critical constraint: BlueBubbles does NOT retry on delivery failure. The handler must return HTTP 200 immediately. All indexing and Aurora dispatch are async (after res.sendStatus(200)).
Dedup strategy
Primary: checkDedup(data.guid) from scripts/lib/webhook-dedup.js — 24-hour TTL in processed_webhook_events.
Fallback: sha256(rawBody) as dedup key when data.guid is absent (e.g., chat-read-status events).
Audit trail
webhook_audit_log— non-blocking write viaauditLog()fromscripts/lib/webhook-audit.js.- BlueBubbles indexer:
recordBluebubblesMessage/recordOmniEventwrite to Supabase for cross-agent event history. - Fatal error handler:
uncaughtException+unhandledRejectionlog structured JSON withhandler: 'imessage'before process exit. - No Discord alert: Errors surface via Aurora’s iMessage responder or gateway health monitor.
Related
- webhook-architecture — Cross-handler governance; imessage-handler is internal-only (Tailscale peer, not FUNNEL-REGISTRY public endpoint)
- _summary — Aurora iMessage responder runner is the primary consumer; fires Claude CLI child process after indexing
- cron-timer-registry — Service unit for this handler (verify in live systemctl state)