Introduction
Cart abandonment affects an average of 70% of e-commerce sessions. A well-designed recovery system can reclaim up to 15% of those lost sales. This tutorial guides you step by step through building a complete solution with Next.js: detecting inactive carts, sending automated emails, and creating a reminder interface. We use Prisma for persistence and Resend for transactional emails.
Prerequisites
- Node.js 20+
- Next.js 15
- Basic TypeScript skills
- Resend account and PostgreSQL database
- Prisma knowledge
Project Initialization
npx create-next-app@latest cart-recovery --yes
cd cart-recovery
npm install prisma @prisma/client resend date-fns
npx prisma initWe initialize a Next.js project and install the essential dependencies for the database and email sending.
Prisma Cart Schema
model Cart {
id String @id @default(cuid())
userId String?
sessionId String @unique
items Json
status String @default("active")
lastActive DateTime @default(now())
createdAt DateTime @default(now())
}The Cart model stores items, status, and last activity. The lastActive field is essential for detecting abandonments.
Cart Update API Route
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function POST(req: Request) {
const { sessionId, items } = await req.json();
const cart = await prisma.cart.upsert({
where: { sessionId },
update: { items, lastActive: new Date() },
create: { sessionId, items }
});
return NextResponse.json(cart);
}This route updates the cart on every change and refreshes lastActive to avoid false abandonment positives.
Abandonment Detection Function
import { prisma } from './prisma';
import { subHours } from 'date-fns';
export async function findAbandonedCarts() {
const threshold = subHours(new Date(), 2);
return prisma.cart.findMany({
where: {
status: 'active',
lastActive: { lt: threshold }
}
});
}This function identifies carts inactive for more than 2 hours, a configurable threshold based on your purchase cycle.
Send Reminder Email
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendAbandonmentEmail(cart: any) {
await resend.emails.send({
from: 'shop@learni.dev',
to: cart.userEmail || 'client@example.com',
subject: 'Your cart is waiting',
html: `<p>You left ${cart.items.length} items.</p>`
});
await prisma.cart.update({
where: { id: cart.id },
data: { status: 'reminded' }
});
}The email is sent via Resend and the status is updated to prevent multiple sends.
Reminder Cron Job
import { findAbandonedCarts } from '@/lib/detectAbandonment';
import { sendAbandonmentEmail } from '@/lib/sendReminder';
export async function GET() {
const carts = await findAbandonedCarts();
for (const cart of carts) {
await sendAbandonmentEmail(cart);
}
return Response.json({ processed: carts.length });
}This cron endpoint can be called by Vercel Cron or an external service every hour.
Best Practices
- Always comply with GDPR and offer clear unsubscribe options
- Personalize emails with the exact cart contents
- Limit reminders to 2 or 3 maximum per cart
- Measure recovery rate with analytics events
- Test emails across multiple email clients
Common Mistakes to Avoid
- Forgetting to update lastActive on every user action
- Sending emails without checking cart status
- Using time thresholds that are too short or too long
- Not handling email sending errors (retry logic)
Further Reading
Discover our advanced courses on modern e-commerce and marketing automation at Learni Group.