Vibestacks LogoVibestacks
IntegrationsStripe & Payments

Lifecycle Emails

Send automated emails for subscription events like welcome messages, trial reminders, and cancellation confirmations. Learn how to set up and customize subscription lifecycle emails.

Lifecycle emails keep users engaged throughout their subscription journey. Vibestacks supports sending emails at key moments like subscription start, trial ending, and cancellation.

Implementation Status

Lifecycle emails are marked as TODO in the codebase. This guide shows you how to implement them.

Email Types

EmailTriggerPurpose
Subscription WelcomeUser subscribesConfirm subscription, highlight features
Trial StartUser starts trialWelcome, set expectations
Trial Ending3 days before trial endsRemind to convert
Trial ExpiredTrial ends without conversionWin-back attempt
Subscription CanceledUser cancelsConfirm, offer to restore
Payment FailedPayment failsPrompt to update payment method

Setting Up Lifecycle Emails

Create Email Templates

Create React Email templates in components/emails/:

components/emails/subscription-welcome.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Text,
} from "@react-email/components";
import { siteConfig } from "@/config/app";

interface SubscriptionWelcomeEmailProps {
  email: string;
  planName: string;
}

export function SubscriptionWelcomeEmail({
  email,
  planName,
}: SubscriptionWelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to {siteConfig.name} {planName}!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Welcome to {planName}! 🎉</Heading>
          
          <Text style={text}>
            Thanks for subscribing. You now have access to all 
            premium features.
          </Text>

          <Link href={`${siteConfig.url}/dashboard`} style={button}>
            Go to Dashboard
          </Link>

          <Text style={footer}>
            - The {siteConfig.name} Team
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

// Styles...
const main = { backgroundColor: "#f6f9fc", fontFamily: "sans-serif" };
const container = { margin: "0 auto", padding: "40px 20px" };
const h1 = { color: "#1f2937", fontSize: "24px" };
const text = { color: "#4b5563", fontSize: "16px" };
const button = { 
  backgroundColor: "#7c3aed", 
  color: "#fff", 
  padding: "12px 24px",
  borderRadius: "6px",
  textDecoration: "none",
};
const footer = { color: "#9ca3af", fontSize: "14px" };

Add Email Functions

Add send functions to lib/email.ts:

lib/email.ts
import { SubscriptionWelcomeEmail } from "@/components/emails/subscription-welcome";
import { TrialStartEmail } from "@/components/emails/trial-start";
import { TrialEndingEmail } from "@/components/emails/trial-ending";
import { SubscriptionCanceledEmail } from "@/components/emails/subscription-canceled";

export async function sendSubscriptionWelcomeEmail({
  email,
  planName,
}: {
  email: string;
  planName: string;
}) {
  const { error } = await resend.emails.send({
    from: siteConfig.emails.from,
    to: [email],
    subject: `Welcome to ${siteConfig.name} ${planName}! 🎉`,
    react: SubscriptionWelcomeEmail({ email, planName }),
  });

  if (error) throw new Error(error.message);
}

export async function sendTrialStartEmail({
  email,
  planName,
  trialDays,
}: {
  email: string;
  planName: string;
  trialDays: number;
}) {
  const { error } = await resend.emails.send({
    from: siteConfig.emails.from,
    to: [email],
    subject: `Your ${trialDays}-day ${planName} trial has started!`,
    react: TrialStartEmail({ email, planName, trialDays }),
  });

  if (error) throw new Error(error.message);
}

export async function sendTrialEndingEmail({
  email,
  planName,
}: {
  email: string;
  planName: string;
}) {
  const { error } = await resend.emails.send({
    from: siteConfig.emails.from,
    to: [email],
    subject: `Your ${planName} trial is ending soon`,
    react: TrialEndingEmail({ email, planName }),
  });

  if (error) throw new Error(error.message);
}

export async function sendSubscriptionCanceledEmail({
  email,
  planName,
  cancelAt,
}: {
  email: string;
  planName: string;
  cancelAt: Date | null;
}) {
  const { error } = await resend.emails.send({
    from: siteConfig.emails.from,
    to: [email],
    subject: `Your ${siteConfig.name} subscription has been canceled`,
    react: SubscriptionCanceledEmail({ email, planName, cancelAt }),
  });

  if (error) throw new Error(error.message);
}

Wire Up Lifecycle Hooks

Update the Stripe plugin in lib/auth.ts:

lib/auth.ts
import { 
  sendSubscriptionWelcomeEmail,
  sendTrialStartEmail,
  sendTrialEndingEmail,
  sendSubscriptionCanceledEmail,
} from "./email";
import { waitUntil } from "@vercel/functions";

// Inside the stripe() plugin configuration:
stripe({
  stripeClient,
  stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
  
  subscription: {
    enabled: true,
    plans: stripePlans.map((plan) => ({
      ...plan,
      // Trial lifecycle (if trial enabled)
      ...(plan.trial?.enabled && {
        freeTrial: {
          days: plan.trial.days,
          onTrialStart: async (subscription) => {
            const user = await getUserById(subscription.referenceId);
            if (user?.email) {
              waitUntil(
                sendTrialStartEmail({
                  email: user.email,
                  planName: plan.name,
                  trialDays: plan.trial.days,
                })
              );
            }
          },
          onTrialEnd: async ({ subscription }) => {
            const user = await getUserById(subscription.referenceId);
            if (user?.email) {
              waitUntil(
                sendTrialEndingEmail({
                  email: user.email,
                  planName: plan.name,
                })
              );
            }
          },
        },
      }),
    })),

    // Subscription lifecycle
    onSubscriptionComplete: async ({ subscription, plan }) => {
      const user = await getUserById(subscription.referenceId);
      if (user?.email) {
        waitUntil(
          sendSubscriptionWelcomeEmail({
            email: user.email,
            planName: plan.name,
          })
        );
      }
    },

    onSubscriptionCancel: async ({ subscription }) => {
      const user = await getUserById(subscription.referenceId);
      if (user?.email) {
        waitUntil(
          sendSubscriptionCanceledEmail({
            email: user.email,
            planName: subscription.plan,
            cancelAt: subscription.cancelAt,
          })
        );
      }
    },
  },
})

Email Templates

Trial Start Email

components/emails/trial-start.tsx
export function TrialStartEmail({ 
  email, 
  planName, 
  trialDays 
}: TrialStartEmailProps) {
  const trialEndDate = new Date();
  trialEndDate.setDate(trialEndDate.getDate() + trialDays);

  return (
    <Html>
      <Preview>Your {trialDays}-day trial has started!</Preview>
      <Body>
        <Heading>Your trial has started! 🚀</Heading>
        
        <Text>
          You have <strong>{trialDays} days</strong> of free access 
          to all {planName} features.
        </Text>

        <Text>
          Your trial ends on {trialEndDate.toLocaleDateString()}.
        </Text>

        <Link href="/dashboard">Start Exploring</Link>
      </Body>
    </Html>
  );
}

Trial Ending Email

components/emails/trial-ending.tsx
export function TrialEndingEmail({ 
  email, 
  planName 
}: TrialEndingEmailProps) {
  return (
    <Html>
      <Preview>Your {planName} trial is ending soon</Preview>
      <Body>
        <Heading>Your trial is ending soon ⏰</Heading>
        
        <Text>
          Your {planName} trial ends in a few days. After that, 
          you'll be automatically subscribed.
        </Text>

        <Text>
          Want to cancel? You can do so before your trial ends.
        </Text>

        <Link href="/dashboard/billing">Manage Subscription</Link>
      </Body>
    </Html>
  );
}

Subscription Canceled Email

components/emails/subscription-canceled.tsx
export function SubscriptionCanceledEmail({ 
  email, 
  planName,
  cancelAt,
}: SubscriptionCanceledEmailProps) {
  return (
    <Html>
      <Preview>Your subscription has been canceled</Preview>
      <Body>
        <Heading>Subscription Canceled</Heading>
        
        <Text>
          Your {planName} subscription has been canceled.
        </Text>

        {cancelAt && (
          <Text>
            You'll continue to have access until{" "}
            {new Date(cancelAt).toLocaleDateString()}.
          </Text>
        )}

        <Text>
          Changed your mind? You can restore your subscription 
          before it ends.
        </Text>

        <Link href="/dashboard/billing">Restore Subscription</Link>
      </Body>
    </Html>
  );
}

Non-Blocking Emails

Always use waitUntil to send emails without blocking the webhook response:

import { waitUntil } from "@vercel/functions";

// Good - non-blocking
waitUntil(sendSubscriptionWelcomeEmail({ email, planName }));

// Bad - blocks webhook response
await sendSubscriptionWelcomeEmail({ email, planName });

This is important because:

  • Webhooks have a 30-second timeout
  • Email sending can be slow
  • Stripe retries on timeout, causing duplicate emails

Preview Emails Locally

Use the React Email dev server to preview templates:

pnpm email

Open http://localhost:3001 to see your email templates.

Testing Emails

Using Stripe CLI

Trigger events to test email flows:

# Test subscription welcome
stripe trigger checkout.session.completed

# Test trial ending (if configured)
stripe trigger customer.subscription.trial_will_end

# Test cancellation
stripe trigger customer.subscription.deleted

Using Resend Test Mode

Resend has a test mode that doesn't send real emails:

# Use the test API key
RESEND_API_KEY="re_test_xxxxx"

Best Practices

Timing

EmailWhen to Send
WelcomeImmediately after subscription
Trial StartImmediately after trial begins
Trial Ending3 days before trial ends
CanceledImmediately after cancellation
Payment FailedImmediately after failure

Content Tips

  1. Keep it short - Users skim emails
  2. One CTA - Single clear action per email
  3. Be helpful - Link to relevant resources
  4. Brand consistently - Match your app's tone

What NOT to Do

  • Don't send too many emails (1-2 per event max)
  • Don't include sensitive data in emails
  • Don't block webhook responses with email sending
  • Don't forget unsubscribe links for marketing emails

Payment Failed Email

Handle failed payments to reduce churn:

onEvent: async (event) => {
  if (event.type === "invoice.payment_failed") {
    const invoice = event.data.object;
    const user = await getUserByStripeCustomerId(invoice.customer as string);
    
    if (user?.email) {
      waitUntil(
        sendPaymentFailedEmail({
          email: user.email,
          amount: invoice.amount_due,
          updateUrl: `${siteConfig.url}/dashboard/billing`,
        })
      );
    }
  }
}

Next Steps