Next.js template available

This guide comes with a one-click deployable demo available on GitHub.

By combining Defer and Resend, send and schedule rich-styled emails in minutes, while keeping full control over their scheduling with cancellations and the Defer Console.

This guide will show you how to implement a monthly usage report email with an attached PDF invoice:

Setup your Defer Token on Vercel

Resend, combined with @react-pdf/renderer will help in sending rich emails.

Defer will help in scheduling monthly emails while respecting Resend’s API rate limiting and avoiding duplicate sends .

Implementing the usage email

A rich email with JSX

Resend combined with its @react-email/components, Tailwind and Defer’s support for JSX enables us to write our monthly usage email with React as follows:

The <MonthlyUsageEmail /> component is then provided to our sendMonthlyUsage() function (example in a NextJS project):

src/defer/sendMonthlyUsage.tsx
import { Resend } from "resend";
import MonthlyUsageEmail from "@/emails/MonthlyUsage";

const resend = new Resend(process.env.RESEND_API_KEY!);

async function sendMonthlyUsage(userId: string) {
  const billingInfo = getBillingInfo(); // ... query DB with `userId`
  const usageInfo = getUsageInfo(); // ... query DB with `userId`

  const title = `You performed ${new Intl.NumberFormat().format(
    usageInfo.executions,
  )} executions in ${billingInfo.period}`;

  await resend.emails.send({
    from: "usage@defer.run",
    to: "charly@defer.run",
    subject: title,
    html: title,
    react: MonthlyUsageEmail({
      firstName: billingInfo.user.firstName,
      title,
      percentPlan: usageInfo.planPercent,
      period: billingInfo.period,
    }),
  });
}

Attach the PDF to the email

Our monthly usage email comes with a PDF file listing all executions performed in the given period.

@react-pdf/renderer, similarly to @react-email/components enable us to create a PDF file using React components, as follows:

src/defer/sendMonthlyUsage.tsx
+import React from "react";
 import { Resend } from "resend";
+import {
+  Page,
+  Text,
+  View,
+  Document,
+  StyleSheet,
+  renderToBuffer,
+} from "@react-pdf/renderer";
 import MonthlyUsageEmail from "@/emails/MonthlyUsage";

+const styles = StyleSheet.create({
+  // ...
+});
+
+// PDF file listing the executions with associated usage
+const ExecutionsDetail = () => (
+  <Document>
+    <Page size="A4" style={styles.page}>
+      // ...
+    </Page>
+  </Document>
+);
+
 const resend = new Resend(process.env.RESEND_API_KEY!);

 async function sendMonthlyUsage() {
+  const pdf = await renderToBuffer(<ExecutionsDetail />);
+
   const title = `You performed ${new Intl.NumberFormat().format(
    usageInfo.executions
  )} executions in ${billingInfo.period}`;
@@ -19,5 +57,16 @@
       percentPlan: usageInfo.planPercent,
      period: billingInfo.period,
     }),
+    attachments: [
+      {
+        content: pdf,
+        filename: "detail.pdf",
+      },
+    ],
   });
 }

Resend accepts an attachments option taking an array of files (Buffer and file’s name).

Transform sendMonthlyUsage() into a Background Function

We simply wrap sendMonthlyUsage() with defer() to transform it into a Defer Background Function:

src/defer/sendMonthlyUsage.tsx
 import { Resend } from "resend";
+import { defer } from "@defer/client";
 import MonthlyUsageEmail from "@/emails/MonthlyUsage";

 const resend = new Resend(process.env.RESEND_API_KEY!);
@@ -21,3 +22,8 @@
     }),
   });
 }
+
+export default defer(sendMonthlyUsage, {
+  concurrency: 10,
+  retry: 5,
+});

You’ll notice that we pass to defer() 2 options:

  • a concurrency: 10 to match Resend’s 10 calls/sec policy
  • a retry strategy to recover potential errors

Our sendMonthlyUsage() is now ready to be called to schedule emails.

Scheduling the monthly usage emails

Each customer has a dedicated sliding usage window (e.g.: From August 15th to September 16th).

Send the usage email on a sliding window schedule

We’ll leverage Defer’s delay() feature to match this criteria:

src/app/api/startSubscription/route.ts
import { delay } from "@defer/client";
import sendMonthlyUsage from "@/defer/sendMonthlyUsage";

export const POST = () => {
  const { current_period_end } = // ... create Subscription in Stripe

  const delayedsendMonthlyUsage = delay(sendMonthlyUsage, new Date(current_period_end));
  await delayedsendMonthlyUsage(userId);
}

Each call to sendMonthlyUsage() from the NextJS App will trigger a background execution on Defer.

We need to update sendMonthlyUsage() to reschedule itself for the next billing period, again by using delay() from @defer/client:

src/defer/sendMonthlyUsage.tsx
@@ -1,6 +1,6 @@
 import React from "react";
 import { Resend } from "resend";
-import { defer } from "@defer/client";
+import { defer, delay } from "@defer/client";
 import {
   Page,
   Text,
@@ -41,10 +41,12 @@

 async function sendMonthlyUsage(userId: string) {
   const pdf = await renderToBuffer(<ExecutionsDetail />);
   const billingInfo = // ... query DB with `userId`
   const usageInfo = // ... query DB with `userId`

   const title = `You performed ${new Intl.NumberFormat().format(
     usageInfo.executions
   )} executions in ${billingInfo.period}`;

   await resend.emails.send({
     from: "onboarding@resend.dev",
@@ -52,10 +54,10 @@
     subject: title,
     html: title,
     react: MonthlyUsageEmail({
       firstName: billingInfo.user.firstName,
       title,
       percentPlan: usageInfo.planPercent,
       period: billingInfo.period,
     }),
     attachments: [
       {
@@ -64,9 +66,14 @@
       },
     ],
   });
+
+  const delayedsendMonthlyUsage = delay(deferSendMonthlyUsage, new Date(billingInfo.current_period_end));
+  await delayedsendMonthlyUsage(userId);
 }

-export default defer(sendMonthlyUsage, {
+const deferSendMonthlyUsage = defer(sendMonthlyUsage, {
   concurrency: 10,
   retry: 5,
 });
+
+export default deferSendMonthlyUsage;

What happened here?

The sendMonthlyUsage() became a recursive Background Function, scheduling itself for the next usage period. To achieve this, we needed to move defer(sendMonthlyUsage) to the deferSendMonthlyUsage variable so we could pass it to delay().

Handling unsubscribed customers

Customers do churn and we might want to stop sending them usage emails if they drop in the middle of a billing cycle.

For this, we will leverage Defer’s cancellations mechanism.

First, let’s store the Execution ID of a planned monthly usage email, by updating sendMonthlyUsage() as follows:

sendMonthlyUsage.ts
@@ -68,7 +68,9 @@
   });

   const delayedsendMonthlyUsage = delay(deferSendMonthlyUsage, new Date(billingInfo.current_period_end));
-  await delayedsendMonthlyUsage(userId);
+  const { id: nextEmailIdExecutionId } = await delayedsendMonthlyUsage(userId);
+
+  // save `nextEmailIdExecutionId` to the database
 }

 const deferSendMonthlyUsage = defer(sendMonthlyUsage, {

Then, let’s update our unsubscribe API Routes to cancel any pending monthly usage email:

src/app/api/cancelSubscription/route.ts
import { cancelExecution } from "@defer/client";
import sendMonthlyUsage from "@/defer/sendMonthlyUsage";

export const POST = () => {
  // ... cancel Subscription in Stripe

  const nextEmailIdExecutionId = // query using `session.userId`
    cancelExecution(nextEmailIdExecutionId);
};

Wrapping up

Our application now provides rich and personalized monthly usage emails to our customers.

The Defer Console is your best companion, making it easy to oversee all usage emails:

Setup your Defer Token on Vercel

Or manage specific ones:

Setup your Defer Token on Vercel

Going further