Groundmail

Send transactional email from Convex mutations with durable queuing, idempotent retries, and real-time delivery status via Groundmail webhooks.

Installation

npm install @groundmail/convex

About Groundmail

Groundmail 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.

Benefits

Use cases

how to send transactional email from a Convex mutation

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.

how to add retry logic to Convex email sending without duplicate sends

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.

how to track email delivery status in Convex

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.

how to handle email bounce events in Convex

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.

Frequently asked questions

What happens if a Convex mutation fails after calling sendEmail?

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.

How does @groundmail/convex prevent duplicate emails when a mutation retries?

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.

Which email statuses does the component track?

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.

How do I set up webhook delivery status updates with @groundmail/convex?

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.

Can I use @groundmail/convex in tests without sending real emails?

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.

Links

Components

Open-source building blocks for your Convex app

Browse components in this category.

Loading components