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

PathSourceNetwork
POST / (or configured path)BlueBubbles v1.9.9 on auroras-mac-miniTailscale 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 typeDescription
new-messageNew iMessage received
updated-messageMessage read receipt or status update
chat-read-status-changedChat marked read
hello-worldConnectivity 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 via auditLog() from scripts/lib/webhook-audit.js.
  • BlueBubbles indexer: recordBluebubblesMessage / recordOmniEvent write to Supabase for cross-agent event history.
  • Fatal error handler: uncaughtException + unhandledRejection log structured JSON with handler: 'imessage' before process exit.
  • No Discord alert: Errors surface via Aurora’s iMessage responder or gateway health monitor.
  • 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)