Email SDK
IntegrationsConvex

Install and configure

Mount the Convex component, map provider secrets, create the server client, and set safe defaults.

Install the packages

bun add @opencoredev/convex-email @opencoredev/email-sdk

The component uses the same built-in adapters as Email SDK, but its configuration must be serializable across the Convex component boundary.

Mount the component

Declare only the environment variables your routes need, then map them into the component instance.

convex/convex.config.ts
import convexEmail from "@opencoredev/convex-email/convex.config.js";
import { defineApp } from "convex/server";
import { v } from "convex/values";

const app = defineApp({
  env: {
    RESEND_API_KEY: v.optional(v.string()),
    SMTP_HOST: v.optional(v.string()),
    SMTP_PORT: v.optional(v.string()),
    SMTP_USER: v.optional(v.string()),
    SMTP_PASS: v.optional(v.string()),
  },
});

app.use(convexEmail, {
  env: {
    RESEND_API_KEY: app.env.RESEND_API_KEY,
    SMTP_HOST: app.env.SMTP_HOST,
    SMTP_PORT: app.env.SMTP_PORT,
    SMTP_USER: app.env.SMTP_USER,
    SMTP_PASS: app.env.SMTP_PASS,
  },
});

export default app;

Run bunx convex dev after installing the component so Convex generates components.convexEmail in your app.

Set provider secrets

bunx convex env set RESEND_API_KEY re_xxx

Secrets stay in the Convex deployment. Adapter configuration stores environment-variable names, never raw credentials.

Create the server client

convex/email.ts
import { ConvexEmail } from "@opencoredev/convex-email";
import { components } from "./_generated/api";

export const email = new ConvexEmail(components.convexEmail, {
  adapters: [
    { kind: "resend" },
    { kind: "smtp", name: "backup-smtp" },
  ],
  defaultAdapter: "resend",
  fallbackAdapters: ["backup-smtp"],
  maxAttempts: 3,
  retryBaseMs: 30_000,
});

The client is server-only. Component functions are internal to your Convex backend, so expose app queries and mutations with your own authorization checks.

Store application defaults

setConfig replaces the complete stored configuration. Put it behind an internal mutation or administrator-only action.

convex/emailAdmin.ts
import { internalMutation } from "./_generated/server";
import { email } from "./email";

export const configure = internalMutation({
  args: {},
  handler: async (ctx) => {
    await email.setConfig(ctx, {
      defaultFrom: "Acme <hello@acme.com>",
      testMode: true,
      sandboxTo: ["email-preview@acme.com"],
      maxAttempts: 3,
      retryBaseMs: 30_000,
      cleanupAfterDays: 30,
    });
  },
});

Start with test mode enabled. Move to production by replacing the config with testMode: false after your provider domain, webhook verification, and monitoring are ready.

Serializable adapter options

Custom fetch functions, SMTP TLS objects, and function-valued provider options cannot cross the component boundary. Use the equivalent serializable option or keep that specialized send in an application Node action.

Send and track email

Queue messages and expose reactive state.

On this page