Convex component that durably handles iMessage, RCS, and SMS via Photon with verified webhook ingestion, burst debouncing, and a gRPC outbox pipeline.
npm install @spectrum-ts/convexA 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.