DEV Community

Cover image for Using Resend with Encore
Ivan Cernja for Encore

Posted on • Originally published at encore.dev

Using Resend with Encore

Every application needs to send emails. Welcome messages, password resets, notifications, receipts. But setting up SMTP servers, dealing with deliverability, and managing templates is tedious. Resend provides a modern API that makes email simple.

In this tutorial, we'll build email functionality using Resend. You'll learn how to send transactional emails, use React Email templates, track delivery status, handle webhooks, and store email history in a database.

What is Resend?

Resend is an email API built for developers. It provides:

  • Simple API - Send emails with a clean, modern interface
  • React Email - Build email templates with React components
  • Deliverability - Built-in best practices for inbox delivery
  • Webhooks - Real-time events for delivery, opens, clicks
  • Analytics - Track email performance and engagement
  • Domains - Verify and manage sending domains

Resend handles the complexity of email infrastructure so you can focus on your content.

What we're building

We'll create a backend with complete email functionality:

  • Transactional emails - Welcome emails, password resets, notifications
  • React Email templates - Type-safe, component-based email design
  • Email tracking - Auto-provisioned PostgreSQL database for email history
  • Webhook handling - Track delivery, bounces, and complaints
  • Batch emails - Send to multiple recipients efficiently
  • Domain verification - Set up custom sending domains

The backend will handle all email sending with delivery tracking and analytics.

Getting started


Prefer to skip the setup? Use encore app create --example=ts/resend to start with a complete working example. This tutorial walks through building it from scratch to understand each component.

First, install Encore if you haven't already:

# macOS
brew install encoredev/tap/encore

# Linux
curl -L https://encore.dev/install.sh | bash

# Windows
iwr https://encore.dev/install.ps1 | iex
Enter fullscreen mode Exit fullscreen mode

Create a new Encore application. This will prompt you to create a free Encore account if you don't have one (required for secret management):

encore app create resend-app --example=ts/hello-world
cd resend-app
Enter fullscreen mode Exit fullscreen mode

Setting up Resend

Creating your Resend account

  1. Go to resend.com and sign up for a free account
  2. Navigate to API Keys and create a new key
  3. Verify your domain (required for sending to real email addresses)

Resend offers a generous free tier with 100 emails per day and 3,000 per month.

Resend API Keys Dashboard

Domain verification

Important: To send emails to real recipients, you must verify your domain with Resend. Without domain verification, you can only send test emails to your own verified email address.

To verify your domain:

  1. Go to the Domains section in your Resend dashboard
  2. Click Add Domain and enter your domain (e.g., yourdomain.com)
  3. Add the DNS records (SPF, DKIM, DMARC) provided by Resend to your domain's DNS settings
  4. Wait for DNS propagation (usually 5-15 minutes)
  5. Click Verify in the Resend dashboard

Once verified, update your from addresses in the code to use your domain:

from: "onboarding@yourdomain.com"  // Replace with your verified domain
Enter fullscreen mode Exit fullscreen mode

For testing: If you don't have a domain yet, Resend lets you send test emails to your own verified email address using their sandbox domain (onboarding@resend.dev). This is perfect for development and testing the integration.

Encore Domains Dashboard

Understanding API key permissions

When creating your API key, Resend offers two permission levels:

  • Full Access (full_access): Can create, delete, get, and update any resource. Use this for development and administrative tasks.
  • Sending Access (sending_access): Can only send emails. This is the recommended permission level for production applications following the principle of least privilege.

For production deployments, consider creating a sending access key and optionally restricting it to a specific domain. This limits the blast radius if your API key is ever compromised.

Installing the SDK

Install Resend and React Email:

npm install resend
npm install -D @react-email/components
Enter fullscreen mode Exit fullscreen mode

Backend implementation

Creating the email service

Every Encore service starts with a service definition file (encore.service.ts). Services let you divide your application into logical components. At deploy time, you can decide whether to colocate them in a single process or deploy them as separate microservices, without changing a single line of code:

// email/encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("email");
Enter fullscreen mode Exit fullscreen mode

Configuring Resend

Store your API key securely using Encore's built-in secrets management:

// email/resend.ts
import { Resend } from "resend";
import { secret } from "encore.dev/config";

const resendApiKey = secret("ResendApiKey");

export const resend = new Resend(resendApiKey());
Enter fullscreen mode Exit fullscreen mode

Set your API key for local development:

# Development (use full access for testing)
encore secret set --dev ResendApiKey

# Production (use sending access for security)
encore secret set --prod ResendApiKey
Enter fullscreen mode Exit fullscreen mode

Production Best Practice: Create a separate API key with sending_access permission for production. If you have multiple domains, create domain-specific keys to further isolate access. You can create domain-restricted keys using Resend's API:

// Example: Creating a domain-specific sending key (for admin tools)
const { data } = await resend.apiKeys.create({
  name: 'Production - yourdomain.com',
  permission: 'sending_access',
  domainId: 'your-domain-id', // Get this from Resend dashboard
});
Enter fullscreen mode Exit fullscreen mode

Setting up the database

To track email history and delivery status, create a PostgreSQL database. With Encore, you can create a database by simply defining it in code. The framework automatically provisions the infrastructure locally using Docker.

Create the database instance:

// email/db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";

export const db = new SQLDatabase("email", {
  migrations: "./migrations",
});
Enter fullscreen mode Exit fullscreen mode

Create the migration file:

-- email/migrations/1_create_emails.up.sql
CREATE TABLE emails (
  id TEXT PRIMARY KEY,
  resend_id TEXT UNIQUE,
  recipient TEXT NOT NULL,
  subject TEXT NOT NULL,
  template TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  delivered_at TIMESTAMP,
  opened_at TIMESTAMP,
  clicked_at TIMESTAMP,
  bounced_at TIMESTAMP,
  complained_at TIMESTAMP,
  error TEXT,
  metadata JSONB,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_emails_recipient ON emails(recipient, created_at DESC);
CREATE INDEX idx_emails_status ON emails(status);
CREATE INDEX idx_emails_resend_id ON emails(resend_id);
Enter fullscreen mode Exit fullscreen mode

Creating email templates with React Email

React Email lets you build email templates using React components. Create a welcome email template:

// email/templates/welcome.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import * as React from "react";

interface WelcomeEmailProps {
  name: string;
  loginUrl: string;
}

export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to our platform!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Welcome, {name}!</Heading>
          <Text style={text}>
            Thanks for joining us. We're excited to have you on board.
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={loginUrl}>
              Get Started
            </Button>
          </Section>
          <Text style={footer}>
            If you have any questions, just reply to this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "20px 0 48px",
  marginBottom: "64px",
};

const h1 = {
  color: "#333",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "40px 0",
  padding: "0",
  textAlign: "center" as const,
};

const text = {
  color: "#333",
  fontSize: "16px",
  lineHeight: "26px",
  textAlign: "center" as const,
};

const buttonContainer = {
  textAlign: "center" as const,
  margin: "32px 0",
};

const button = {
  backgroundColor: "#5469d4",
  borderRadius: "4px",
  color: "#fff",
  fontSize: "16px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "200px",
  padding: "12px",
  margin: "0 auto",
};

const footer = {
  color: "#8898aa",
  fontSize: "14px",
  lineHeight: "24px",
  textAlign: "center" as const,
  marginTop: "32px",
};
Enter fullscreen mode Exit fullscreen mode

Sending emails

Create an endpoint to send welcome emails:

// email/send.ts
import { api } from "encore.dev/api";
import { resend } from "./resend";
import { db } from "./db";
import { render } from "@react-email/components";
import { WelcomeEmail } from "./templates/welcome";
import log from "encore.dev/log";

interface SendWelcomeEmailRequest {
  to: string;
  name: string;
  loginUrl?: string;
}

interface SendWelcomeEmailResponse {
  id: string;
  resendId: string;
}

export const sendWelcomeEmail = api(
  { expose: true, method: "POST", path: "/email/welcome" },
  async (req: SendWelcomeEmailRequest): Promise<SendWelcomeEmailResponse> => {
    const emailId = `email-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    log.info("Sending welcome email", { to: req.to, emailId });

    // Render the React Email template to HTML
    const html = render(
      WelcomeEmail({
        name: req.name,
        loginUrl: req.loginUrl || "https://yourapp.com/login",
      })
    );

    try {
      // Send email via Resend
      const { data, error } = await resend.emails.send({
        from: "onboarding@yourdomain.com",
        to: req.to,
        subject: "Welcome to our platform!",
        html,
      });

      if (error) {
        throw new Error(`Failed to send email: ${error.message}`);
      }

      // Store email record
      await db.exec`
        INSERT INTO emails (id, resend_id, recipient, subject, template, status, metadata)
        VALUES (
          ${emailId},
          ${data!.id},
          ${req.to},
          ${"Welcome to our platform!"},
          ${"welcome"},
          ${"sent"},
          ${JSON.stringify({ name: req.name })}
        )
      `;

      log.info("Welcome email sent", { emailId, resendId: data!.id });

      return {
        id: emailId,
        resendId: data!.id, // Resend's unique email ID for tracking
      };
    } catch (error) {
      // Store failed email
      const errorMessage = error instanceof Error ? error.message : "Unknown error";

      await db.exec`
        INSERT INTO emails (id, recipient, subject, template, status, error)
        VALUES (
          ${emailId},
          ${req.to},
          ${"Welcome to our platform!"},
          ${"welcome"},
          ${"failed"},
          ${errorMessage}
        )
      `;

      throw error;
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

Password reset emails

Create another template for password resets:

// email/templates/password-reset.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
} from "@react-email/components";
import * as React from "react";

interface PasswordResetEmailProps {
  name: string;
  resetUrl: string;
}

export function PasswordResetEmail({ name, resetUrl }: PasswordResetEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Reset your password</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Password Reset</Heading>
          <Text style={text}>Hi {name},</Text>
          <Text style={text}>
            We received a request to reset your password. Click the button below to
            choose a new password:
          </Text>
          <Section style={buttonContainer}>
            <Button style={button} href={resetUrl}>
              Reset Password
            </Button>
          </Section>
          <Text style={footer}>
            If you didn't request this, you can safely ignore this email.
          </Text>
          <Text style={footer}>This link will expire in 1 hour.</Text>
        </Container>
      </Body>
    </Html>
  );
}

// Styles similar to welcome email...
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "20px 0 48px",
  marginBottom: "64px",
};

const h1 = {
  color: "#333",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "40px 0",
  padding: "0",
  textAlign: "center" as const,
};

const text = {
  color: "#333",
  fontSize: "16px",
  lineHeight: "26px",
  textAlign: "left" as const,
  padding: "0 20px",
};

const buttonContainer = {
  textAlign: "center" as const,
  margin: "32px 0",
};

const button = {
  backgroundColor: "#dc3545",
  borderRadius: "4px",
  color: "#fff",
  fontSize: "16px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
  width: "200px",
  padding: "12px",
  margin: "0 auto",
};

const footer = {
  color: "#8898aa",
  fontSize: "14px",
  lineHeight: "24px",
  textAlign: "center" as const,
  marginTop: "16px",
};
Enter fullscreen mode Exit fullscreen mode

And the endpoint:

// email/send.ts (continued)
import { PasswordResetEmail } from "./templates/password-reset";

interface SendPasswordResetRequest {
  to: string;
  name: string;
  resetUrl: string;
}

interface SendPasswordResetResponse {
  id: string;
  resendId: string;
}

export const sendPasswordReset = api(
  { expose: true, method: "POST", path: "/email/password-reset" },
  async (req: SendPasswordResetRequest): Promise<SendPasswordResetResponse> => {
    const emailId = `email-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;

    const html = render(
      PasswordResetEmail({
        name: req.name,
        resetUrl: req.resetUrl,
      })
    );

    const { data, error } = await resend.emails.send({
      from: "security@yourdomain.com",
      to: req.to,
      subject: "Reset your password",
      html,
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    await db.exec`
      INSERT INTO emails (id, resend_id, recipient, subject, template, status, metadata)
      VALUES (
        ${emailId},
        ${data!.id},
        ${req.to},
        ${"Reset your password"},
        ${"password-reset"},
        ${"sent"},
        ${JSON.stringify({ name: req.name })}
      )
    `;

    return {
      id: emailId,
      resendId: data!.id,
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

Handling webhooks

Resend sends webhooks for email events (delivered, opened, bounced, etc.). Handle these to update your database:

// email/webhooks.ts
import { api } from "encore.dev/api";
import { db } from "./db";
import log from "encore.dev/log";

interface ResendWebhookEvent {
  type: string;
  created_at: string;
  data: {
    email_id: string;
    from: string;
    to: string[];
    subject: string;
    created_at: string;
  };
}

export const handleWebhook = api.raw(
  { expose: true, path: "/webhooks/resend", method: "POST" },
  async (req, res) => {
    const event = (await req.json()) as ResendWebhookEvent;

    log.info("Received Resend webhook", { type: event.type, emailId: event.data.email_id });

    switch (event.type) {
      case "email.sent":
        await db.exec`
          UPDATE emails
          SET status = 'sent'
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.delivered":
        await db.exec`
          UPDATE emails
          SET status = 'delivered', delivered_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.opened":
        await db.exec`
          UPDATE emails
          SET opened_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.clicked":
        await db.exec`
          UPDATE emails
          SET clicked_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.bounced":
        await db.exec`
          UPDATE emails
          SET status = 'bounced', bounced_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;

      case "email.complained":
        await db.exec`
          UPDATE emails
          SET status = 'complained', complained_at = NOW()
          WHERE resend_id = ${event.data.email_id}
        `;
        break;
    }

    res.writeHead(200);
    res.end();
  }
);
Enter fullscreen mode Exit fullscreen mode

Configure the webhook URL in Resend Dashboard: https://your-domain.com/webhooks/resend

About Resend Email IDs: Every email sent through Resend gets a unique ID (like re_ABC123xyz). Store this ID in your database to:

  • Track email status through webhooks
  • Query individual email details via Resend's API
  • Debug delivery issues by cross-referencing with Resend's dashboard
  • Provide customer support with specific email references

Listing email history

Create an endpoint to retrieve sent emails:

// email/send.ts (continued)
interface EmailRecord {
  id: string;
  recipient: string;
  subject: string;
  template: string | null;
  status: string;
  deliveredAt: Date | null;
  openedAt: Date | null;
  clickedAt: Date | null;
  createdAt: Date;
}

interface ListEmailsRequest {
  recipient?: string;
  limit?: number;
}

interface ListEmailsResponse {
  emails: EmailRecord[];
}

export const listEmails = api(
  { expose: true, method: "GET", path: "/email/list" },
  async (req: ListEmailsRequest): Promise<ListEmailsResponse> => {
    const limit = req.limit || 50;

    let query;
    if (req.recipient) {
      query = db.query<{
        id: string;
        recipient: string;
        subject: string;
        template: string | null;
        status: string;
        delivered_at: Date | null;
        opened_at: Date | null;
        clicked_at: Date | null;
        created_at: Date;
      }>`
        SELECT id, recipient, subject, template, status, delivered_at, opened_at, clicked_at, created_at
        FROM emails
        WHERE recipient = ${req.recipient}
        ORDER BY created_at DESC
        LIMIT ${limit}
      `;
    } else {
      query = db.query<{
        id: string;
        recipient: string;
        subject: string;
        template: string | null;
        status: string;
        delivered_at: Date | null;
        opened_at: Date | null;
        clicked_at: Date | null;
        created_at: Date;
      }>`
        SELECT id, recipient, subject, template, status, delivered_at, opened_at, clicked_at, created_at
        FROM emails
        ORDER BY created_at DESC
        LIMIT ${limit}
      `;
    }

    const emails: EmailRecord[] = [];
    for await (const row of query) {
      emails.push({
        id: row.id,
        recipient: row.recipient,
        subject: row.subject,
        template: row.template,
        status: row.status,
        deliveredAt: row.delivered_at,
        openedAt: row.opened_at,
        clickedAt: row.clicked_at,
        createdAt: row.created_at,
      });
    }

    return { emails };
  }
);
Enter fullscreen mode Exit fullscreen mode

Testing locally

Start your backend (make sure Docker is running first):

encore run
Enter fullscreen mode Exit fullscreen mode

Your API is now running locally. Open the local development dashboard at http://localhost:9400 to explore your API.

Send a welcome email

curl -X POST http://localhost:4000/email/welcome \
  -H "Content-Type: application/json" \
  -d '{
    "to": "your-verified-email@gmail.com",
    "name": "John Doe",
    "loginUrl": "https://yourapp.com/login"
  }'
Enter fullscreen mode Exit fullscreen mode

Note: Replace your-verified-email@gmail.com with the email address you used to sign up for Resend, or any email address on your verified domain.

Response:

{
  "id": "email-1234567890-abc",
  "resendId": "re_ABC123xyz"
}
Enter fullscreen mode Exit fullscreen mode

Send a password reset email

curl -X POST http://localhost:4000/email/password-reset \
  -H "Content-Type: application/json" \
  -d '{
    "to": "your-verified-email@gmail.com",
    "name": "John Doe",
    "resetUrl": "https://yourapp.com/reset/token123"
  }'
Enter fullscreen mode Exit fullscreen mode

List sent emails

# All emails
curl http://localhost:4000/email/list

# For specific recipient
curl "http://localhost:4000/email/list?recipient=your-verified-email@gmail.com"
Enter fullscreen mode Exit fullscreen mode

Exploring the local dashboard

The local development dashboard at http://localhost:9400 provides:

  • API Explorer - Test email sending interactively
  • Service Catalog - Auto-generated API documentation
  • Architecture Diagram - Visual representation of your services
  • Distributed Tracing - See the full flow including Resend API calls
  • Database Explorer - Browse email history, delivery status, and engagement

Encore distributed tracing showing email sending flow

The database explorer shows all your email records with delivery tracking:

Encore database explorer showing email history

Advanced features

Additional email fields

Resend supports many additional email fields beyond the basics. Here's how to use CC, BCC, reply-to addresses, tags, and headers:

export const sendAdvancedEmail = api(
  { expose: true, method: "POST", path: "/email/advanced" },
  async (req: {
    to: string[];
    subject: string;
    html: string;
  }) => {
    const { data, error } = await resend.emails.send({
      from: "notifications@yourdomain.com",
      to: req.to,
      subject: req.subject,
      html: req.html,
      // CC and BCC recipients
      cc: ["manager@yourdomain.com"],
      bcc: ["archive@yourdomain.com"],
      // Reply-to address (different from 'from')
      replyTo: "support@yourdomain.com",
      // Tags for filtering and analytics
      tags: [
        { name: "category", value: "transactional" },
        { name: "priority", value: "high" },
      ],
      // Custom headers
      headers: {
        "X-Entity-Ref-ID": "123456",
      },
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    return { id: data!.id };
  }
);
Enter fullscreen mode Exit fullscreen mode

Use cases for these fields:

  • CC/BCC: Copy managers on customer communications, archive all emails to a compliance inbox
  • Reply-To: Direct replies to support@ even when sending from noreply@ or automated systems
  • Tags: Filter and analyze emails in Resend dashboard by campaign, user segment, or priority
  • Headers: Add custom tracking IDs, reference numbers, or metadata for your internal systems

Batch emails

Send to multiple recipients efficiently:

export const sendBatch = api(
  { expose: true, method: "POST", path: "/email/batch" },
  async (req: {
    recipients: Array<{ email: string; name: string }>;
    subject: string;
    html: string;
  }) => {
    const emails = req.recipients.map((recipient) => ({
      from: "notifications@yourdomain.com",
      to: recipient.email,
      subject: req.subject,
      html: req.html,
    }));

    const { data, error } = await resend.batch.send(emails);

    if (error) {
      throw new Error(`Batch send failed: ${error.message}`);
    }

    // Store each email in database
    for (let i = 0; i < data!.data.length; i++) {
      const emailId = `email-${Date.now()}-${i}`;
      await db.exec`
        INSERT INTO emails (id, resend_id, recipient, subject, status)
        VALUES (
          ${emailId},
          ${data!.data[i].id},
          ${req.recipients[i].email},
          ${req.subject},
          ${"sent"}
        )
      `;
    }

    return { sent: data!.data.length };
  }
);
Enter fullscreen mode Exit fullscreen mode

Email with attachments

Send emails with file attachments:

export const sendWithAttachment = api(
  { expose: true, method: "POST", path: "/email/attachment" },
  async (req: {
    to: string;
    subject: string;
    html: string;
    attachment: {
      filename: string;
      content: string; // Base64 encoded
    };
  }) => {
    const { data, error } = await resend.emails.send({
      from: "documents@yourdomain.com",
      to: req.to,
      subject: req.subject,
      html: req.html,
      attachments: [
        {
          filename: req.attachment.filename,
          content: Buffer.from(req.attachment.content, "base64"),
        },
      ],
    });

    if (error) {
      throw new Error(`Failed to send email: ${error.message}`);
    }

    return { id: data!.id };
  }
);
Enter fullscreen mode Exit fullscreen mode

Scheduled emails

Use Encore's cron jobs to send scheduled emails:

import { CronJob } from "encore.dev/cron";

const _ = new CronJob("weekly-digest", {
  title: "Send weekly digest emails",
  every: "0 9 * * 1", // Every Monday at 9 AM
  endpoint: sendWeeklyDigest,
});

export const sendWeeklyDigest = api(
  { expose: false },
  async (): Promise<void> => {
    // Fetch users who want weekly digests
    // Send digest email to each user
    log.info("Sending weekly digest emails");
  }
);
Enter fullscreen mode Exit fullscreen mode

Email analytics

Track email performance:

interface EmailAnalyticsResponse {
  totalSent: number;
  delivered: number;
  opened: number;
  clicked: number;
  bounced: number;
  deliveryRate: number;
  openRate: number;
  clickRate: number;
}

export const getAnalytics = api(
  { expose: true, method: "GET", path: "/email/analytics" },
  async (): Promise<EmailAnalyticsResponse> => {
    const stats = await db.queryRow<{
      total: number;
      delivered: number;
      opened: number;
      clicked: number;
      bounced: number;
    }>`
      SELECT
        COUNT(*) as total,
        COUNT(CASE WHEN status = 'delivered' THEN 1 END) as delivered,
        COUNT(CASE WHEN opened_at IS NOT NULL THEN 1 END) as opened,
        COUNT(CASE WHEN clicked_at IS NOT NULL THEN 1 END) as clicked,
        COUNT(CASE WHEN status = 'bounced' THEN 1 END) as bounced
      FROM emails
      WHERE created_at > NOW() - INTERVAL '30 days'
    `;

    const total = Number(stats!.total);
    const delivered = Number(stats!.delivered);
    const opened = Number(stats!.opened);
    const clicked = Number(stats!.clicked);

    return {
      totalSent: total,
      delivered,
      opened,
      clicked,
      bounced: Number(stats!.bounced),
      deliveryRate: delivered / total,
      openRate: opened / delivered,
      clickRate: clicked / opened,
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

Use cases

User onboarding flow

Send a series of onboarding emails:

export const startOnboarding = api(
  { expose: true, method: "POST", path: "/onboarding/start" },
  async (req: { email: string; name: string }) => {
    // Send immediate welcome email
    await sendWelcomeEmail({
      to: req.email,
      name: req.name,
    });

    // Schedule follow-up emails using your job queue
    // Day 1: Getting started tips
    // Day 3: Feature highlights
    // Day 7: Success stories

    return { started: true };
  }
);
Enter fullscreen mode Exit fullscreen mode

Order confirmations

Send transactional receipts:

export const sendOrderConfirmation = api(
  { expose: true, method: "POST", path: "/email/order" },
  async (req: {
    to: string;
    orderId: string;
    items: Array<{ name: string; price: number }>;
    total: number;
  }) => {
    const html = render(
      OrderConfirmationEmail({
        orderId: req.orderId,
        items: req.items,
        total: req.total,
      })
    );

    const { data } = await resend.emails.send({
      from: "orders@yourdomain.com",
      to: req.to,
      subject: `Order confirmation - ${req.orderId}`,
      html,
    });

    return { sent: true, id: data!.id };
  }
);
Enter fullscreen mode Exit fullscreen mode

Notification digests

Aggregate notifications into daily/weekly emails:

export const sendDigest = api(
  { expose: true, method: "POST", path: "/email/digest" },
  async (req: {
    to: string;
    notifications: Array<{ title: string; message: string; url: string }>;
  }) => {
    const html = render(
      DigestEmail({
        notifications: req.notifications,
      })
    );

    await resend.emails.send({
      from: "digest@yourdomain.com",
      to: req.to,
      subject: `You have ${req.notifications.length} new notifications`,
      html,
    });

    return { sent: true };
  }
);
Enter fullscreen mode Exit fullscreen mode

Frontend integration

From your frontend, trigger email sends:

// Example React component
import { useState } from "react";

function PasswordResetForm() {
  const [email, setEmail] = useState("");
  const [sent, setSent] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await fetch("http://localhost:4000/email/password-reset", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        to: email,
        name: "User", // Get from your user database
        resetUrl: `https://yourapp.com/reset/${generateToken()}`,
      }),
    });

    setSent(true);
  };

  if (sent) {
    return <p>Check your email for reset instructions!</p>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
      />
      <button type="submit">Reset Password</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

For complete frontend integration guides, see the frontend integration documentation.

Deployment

Self-hosting

See the self-hosting instructions for how to use encore build docker to create a Docker image and configure it.

Encore Cloud Platform

Deploy your application using git push encore:

git add -A .
git commit -m "Add Resend email functionality"
git push encore
Enter fullscreen mode Exit fullscreen mode

Set your production secret:

encore secret set --prod ResendApiKey
Enter fullscreen mode Exit fullscreen mode

Note: Encore Cloud is great for prototyping and development with fair use limits. For production workloads, you can connect your AWS or GCP account and Encore will provision infrastructure directly in your cloud account.

Next steps

If you found this tutorial helpful, consider starring Encore on GitHub to help others discover it.

Top comments (0)