Email Queue

Durable transactional email queue for Convex with idempotent enqueue, host-driven SMTP and JMAP transport, and automatic retry with configurable budgets.

Installation

npm install @vllnt/convex-email

About Email Queue

Email Queue is a durable, transport-agnostic outbound transactional email queue as a Convex component: the host enqueues a message, its own action sends it over SMTP or JMAP, and delivery status streams live to clients via reactive Convex queries. The component never contacts a mail provider — the host claims each message (`markSending`), dispatches it, and reports the outcome (`markSent`/`markFailed`); `markFailed` re-queues until `maxAttempts`, then records a terminal `failed` that no late callback can overwrite. Tables are sandboxed, enqueue is idempotent via `idempotencyKey`, time is server-sourced, and it runs identically on Convex Cloud and self-hosted convex-backend.

Benefits

Use cases

how to send transactional email from a Convex mutation without blocking

The Email Queue component lets you call email.enqueue() inside a Convex mutation to record the send intent, then schedule a separate internalAction to claim the message with markSending(), dispatch it over SMTP or JMAP, and report the outcome with markSent() or markFailed(). The mutation returns immediately with a messageId while delivery happens asynchronously.

how to add retry logic to outbound email in Convex

Calling email.markFailed() on a message re-queues it automatically until the attempt count reaches maxAttempts, at which point it transitions to a terminal failed state. The return value includes a retried boolean so the host action can schedule another flush attempt with a backoff delay using ctx.scheduler.runAfter().

how to send email from Convex without a Node.js action

The JMAP adapter at @vllnt/convex-email/jmap is fetch-based and runs in a plain Convex action with no use node directive and no extra dependencies. It supports any JMAP server including Stalwart, Fastmail, and Cyrus, and requires only a session URL and bearer token.

idempotent email sending in Convex to prevent double sends on retry

Pass an idempotencyKey option to email.enqueue() keyed to a stable business identifier such as welcome:{userId}. If the same key is enqueued again, the component returns deduplicated: true and skips insertion, so retrying the enclosing mutation never creates a duplicate message.

Frequently asked questions

Does @vllnt/convex-email send emails itself or does the host application handle transport?

@vllnt/convex-email is host-driven: the component records queue state and enforces delivery semantics, but it never contacts an email provider directly. The host application claims a message with markSending(), calls its own transport (SMTP or JMAP), and then reports the outcome with markSent() or markFailed(). This keeps credentials and provider logic entirely in the host.

What transports does @vllnt/convex-email support?

The component ships two optional adapters. The SMTP adapter at @vllnt/convex-email/smtp wraps nodemailer and requires a use node action. The JMAP adapter at @vllnt/convex-email/jmap is fetch-based, runs in a plain Convex action without use node, and has zero additional dependencies. Both adapters work with any compliant server, not specific vendors.

How does retry work in the Email Queue component?

Calling email.markFailed() re-queues the message automatically and increments its attempt counter. Once attempts reaches maxAttempts the message transitions to a terminal failed state and is never re-queued. The markFailed() return value includes a retried boolean so the host action knows whether to schedule another delivery attempt.

Can I check the delivery status of an email in a Convex query?

Yes. email.get(ctx, messageId) returns a MessageView for a single message, and email.listByStatus(ctx, status, paginationOpts) returns a paginated list filtered by status. Both are standard Convex queries, so they update reactively in clients that subscribe to them.

What version of Convex is required and are there third-party runtime dependencies?

The queue core requires convex@^1.41.0 and has zero third-party runtime dependencies. The SMTP adapter has nodemailer@^8.0.4 as an optional peer dependency that you only install if you use that transport. The JMAP adapter adds no dependencies beyond Convex itself.

Links