Loading components
Send transactional email from Convex mutations with durable queuing, idempotent retries, and real-time delivery status via Groundmail webhooks.
npm install @groundmail/convexGroundmail for Convex is a component that sends transactional email from mutations with full transactional safety: the email is queued atomically with the rest of the mutation, sent in a background action, and retried on temporary failures using idempotency keys so the recipient never receives duplicates. Delivery status (delivered, bounced, clicked, etc.) is written back to your Convex database via signed webhooks, making it queryable and reactive like any other Convex data.
The @groundmail/convex component exposes a sendEmail method you call directly inside a mutation handler. The email is stored in your Convex database within the same transaction, then delivered in the background by a Convex action. If the mutation rolls back, nothing is sent.
Pass an idempotencyKey such as 'welcome:${userId}' to groundmail.sendEmail. A second call with the same key returns the original email id and sends nothing. Every retry to Groundmail also carries the same Idempotency-Key header, so temporary failures are retried without ever delivering the email twice.
The @groundmail/convex component stores delivery status on each email document in your Convex database. Call groundmail.status(ctx, emailId) from a query to get the current status, bounceType, and engagement flags. Wire up the webhook handler in convex/http.ts to receive delivered, bounced, opened, and clicked events from Groundmail and keep statuses current reactively.
Pass an internalMutation reference as onEmailEvent when constructing the Groundmail client. The mutation receives the event type and payload including data.tags, so a user_id tag lets you identify and update the affected user. The component also fires an email.failed event with a bounce_reason when a send is rejected or exhausts retries.
The @groundmail/convex component stores the email in the same Convex transaction as your mutation. If the mutation fails and rolls back, the email record is never written and nothing is sent to Groundmail. This is the core durability guarantee of the component.
Pass an idempotencyKey tied to the business event, such as 'receipt:${orderId}'. If sendEmail is called again with the same key, the component returns the first email's id and makes no new request to Groundmail. Every HTTP request to Groundmail also carries an Idempotency-Key header, so even if the background action retries after a network error, Groundmail will not deliver the email a second time.
The @groundmail/convex component tracks queued, sending, scheduled, sent, delivery_delayed, delivered, held, bounced, failed, suppressed, cancelled, and unknown. Statuses beyond sent require a Groundmail webhook to be configured. The status field is stored on a Convex document, so it is queryable and reactive like any other Convex data.
Add an HTTP route in convex/http.ts that calls groundmail.handleGroundmailEventWebhook(ctx, req). Then create a webhook endpoint in the Groundmail dashboard or CLI pointing at your deployment's .convex.site URL, copy the whsec_ signing secret to your deployment with npx convex env set GROUNDMAIL_WEBHOOK_SECRET, and the handler will verify signatures and update email documents automatically.
Yes. Set GROUNDMAIL_API_KEY to a gm_test_ key and no emails are delivered. You can also use Groundmail simulator addresses such as bounce@simulator.groundmail.dev to exercise the full webhook flow in a test environment. For unit testing with convex-test, import groundmailTest from @groundmail/convex/test and call groundmailTest.register(t) to register the component.