iMessage (Photon)

Convex component that durably handles iMessage, RCS, and SMS via Photon with verified webhook ingestion, burst debouncing, and a gRPC outbox pipeline.

Installation

npm install @spectrum-ts/convex

About iMessage (Photon)

A Convex component that integrates [Photon](https://photon.codes)'s iMessage/RCS/SMS platform into a Convex backend without requiring a long-lived process. It solves the fundamental incompatibility between Photon's streaming message loop and Convex's isolated execution model by verifying and persisting inbound webhook deliveries to a durable table before returning the HTTP 200, then handing off all subsequent processing to Convex's scheduler. The component ships burst debouncing, in-flight cancellation, carry-forward context, and a paced outbox pipeline rather than leaving those to be reimplemented per app.

Benefits

Use cases

how to receive iMessage webhooks in a Convex backend

The @spectrum-ts/convex component registers an HTTP route via spectrum.registerRoutes(http) that verifies the Photon HMAC signature and writes the delivery to a Convex table before returning 200. Because the write is durable before the response, Convex's scheduler can process the message even if the isolate is torn down immediately after. No long-lived process is needed on the Convex side.

how to build an iMessage AI agent with Convex that handles rapid follow-up messages

Set mode: 'collapse' when constructing the Spectrum instance. In this mode, a new inbound message cancels any in-flight reply that has not yet been sent and carries all prior messages forward as context, so the agent answers the final intent rather than each intermediate message. The isCancelled check inside your internalAction lets you abort after slow operations like LLM calls.

send iMessage from a Convex action using Photon gRPC

Call spectrum.send(ctx, { spaceId, chainId, content }) inside a Convex Node action. The component queues the send to an outbox and dispatches it through a gRPC transport via createCloudSender from @spectrum-ts/convex/sender. You must install the gRPC peer dependencies and add them to node.externalPackages in convex.json, and include a _workaround.ts re-export so Convex bundles the optional peers.

deduplicate webhook deliveries in Convex messaging component

The @spectrum-ts/convex component deduplicates inbound deliveries on the provider message ID, so redeliveries of the same message do not create duplicate rows or trigger duplicate handler calls. Outbound sends are also deduplicated at the outbox using a clientGuid derived from (chainId, seq), preventing the same logical send from being queued twice.

Frequently asked questions

Why can't I use Photon's app.messages loop or app.webhook() directly in Convex?

Photon's app.messages loop requires a long-lived process, which Convex isolates do not support. The app.webhook() handler runs after the HTTP response is returned, but a Convex isolate is frozen at that point, so any work done there is unreliable. The @spectrum-ts/convex component solves this by writing the verified delivery to a Convex table before the 200 response is sent, making it durable, and then scheduling all subsequent work through Convex's scheduler.

What environment variables does the iMessage (Photon) Convex component require?

The @spectrum-ts/convex component requires three environment variables set in the Convex dashboard: SPECTRUM_WEBHOOK_SECRET for verifying the HMAC signature on incoming webhooks, SPECTRUM_PROJECT_ID for identifying your Spectrum Cloud project, and SPECTRUM_PROJECT_SECRET for authenticating with Spectrum Cloud.

What is the difference between immediate and collapse mode in @spectrum-ts/convex?

In immediate mode (the default), every inbound message triggers a separate onBatch handler call with a single message and no carried context, which is appropriate for cheap replies like echo bots or commands. In collapse mode, a burst of messages within a settling window is coalesced into one handler call, and a new message while a reply is in flight cancels that reply and carries all messages forward as prior context, which is appropriate for expensive LLM-based replies.

Why do gRPC sends fail with a peer dependency error even when the packages are in package.json?

Convex only installs externalPackages entries if the bundled code statically imports them. The gRPC peers (nice-grpc, nice-grpc-common, @grpc/grpc-js) are optional peers of @photon-ai/advanced-imessage and are loaded dynamically, so the bundler never references them and they are not installed. The fix is to add a convex/_workaround.ts file that re-exports all three packages, giving the bundler the static reference it needs to include them.

What message content types can be sent through the @spectrum-ts/convex outbox?

The @spectrum-ts/convex outbox currently supports text and markdown content. The outbox serializes content to JSON for durability and replays it later, and there is no public Photon SDK API to send a pre-resolved Content object back. Attachments, polls, and other rich content types are not supported and will throw a clear error rather than sending incorrect content.

Links