Wraps Logo
Next.js + Vercel + AWS SES

Stop Wrestling with
AWS SES Configuration

Deploy production-ready email infrastructure to your AWS account in minutes.
No stored credentials. No access keys. You own everything.

12 min readWraps Team
terminal — wraps email init

Setting up AWS SES for a Next.js app deployed on Vercel usually means hours of configuration, sandbox approval headaches, and hardcoding AWS credentials as environment variables. There's a better way.

In this guide, you'll learn how to deploy production-ready email infrastructure to your AWS account in minutes—and send emails from your Vercel-hosted Next.js app without storing a single credential.

The Problem with Traditional SES Setup

If you've tried to set up AWS SES yourself, you've probably hit these walls:

  • Sandbox limbo: Writing essay-length justifications for production access, getting cryptic rejections with no feedback
  • DNS configuration: Manually adding DKIM records, waiting 72 hours for propagation, troubleshooting verification failures
  • Credential management: Creating IAM users, generating access keys, rotating them periodically, storing them in Vercel's environment variables—see our SES setup guide for the full picture
  • Missing DX: No SDK, no dashboard, no event tracking—just raw API calls

Most developers abandon SES setup and pay 10x more for alternatives like Resend or SendGrid. But what if you could get the DX of Resend with the economics of AWS—and actually own the infrastructure?

What We're Building

By the end of this tutorial, you'll have:

  1. SES infrastructure deployed to your AWS account (domains, event tracking, email history)
  2. OIDC authentication so your Vercel functions can send email without stored credentials
  3. A type-safe SDK for sending emails from Next.js API routes or Server Actions

The setup takes about 2 minutes. Seriously.

Prerequisites

  • An AWS account with CLI access configured (aws configure)
  • A Vercel account with a Next.js project
  • A domain you own (for sending emails)
  • Node.js 20+

Zero Stored Credentials

OIDC authentication means your Vercel functions get temporary AWS credentials automatically. No access keys to leak or rotate.

AWS Economics

Pay AWS directly for sending ($0.10/1K emails). No middleman markup. A startup sending 50K emails/month pays ~$5 to AWS.

You Own Everything

Infrastructure deploys to YOUR AWS account. Your domain, your data, your reputation. Leave anytime, keep everything.

Step-by-Step Setup Guide

From zero to sending emails in minutes

1

Install the Wraps CLI

Install the CLI globally, or run it directly with npx:

terminal
npm install -g @wraps.dev/cli

Or run directly without installing:

terminal
npx @wraps.dev/cli email init
2

Deploy Email Infrastructure

Run the init command to deploy SES infrastructure to your AWS account:

terminal
wraps email init

The CLI will walk you through a few prompts:

  • Select your hosting provider — choose Vercel (recommended for Next.js). This sets up an OIDC trust relationship between Vercel and your AWS account.
  • Enter your Vercel team slug and project name — these configure the OIDC trust policy so only your Vercel project can assume the role.
  • Choose a configuration preset — for most apps, Production is the sweet spot (real-time event tracking, 90-day email history).

The CLI deploys via Pulumi: IAM Role, OIDC Provider, SES Configuration Set, EventBridge Rule, SQS Queue + DLQ, Lambda Processor, and DynamoDB Tables.

Total time: ~2 minutes.

3

Add and Verify Your Domain

Add a sending domain and get DKIM records for your DNS:

terminal
wraps email domains add -d yourdomain.com

The CLI returns DKIM tokens for your DNS. Add these CNAME records to your DNS provider. Verification usually takes a few minutes, though DNS propagation can take up to 72 hours in some cases.

Check status anytime:

terminal
wraps email domains list
4

Install the SDK

Add the type-safe email SDK to your Next.js project:

terminal
npm install @wraps.dev/email
5

Send Your First Email

Here's a Next.js API route that sends a welcome email:

app/api/send-welcome/route.ts
import { Wraps } from '@wraps.dev/email';

const wraps = new Wraps();

export async function POST(request: Request) {
  const { email, name } = await request.json();

  const result = await wraps.emails.send({
    from: 'hello@yourdomain.com',
    to: email,
    subject: `Welcome to the team, ${name}!`,
    html: `
      <h1>Hey ${name}!</h1>
      <p>Thanks for signing up. We're excited to have you.</p>
      <p>If you have any questions, just reply to this email.</p>
    `,
  });

  return Response.json({
    success: true,
    messageId: result.messageId,
  });
}

Or use it in a Server Action:

app/actions/send-email.ts
'use server';

import { Wraps } from '@wraps.dev/email';

const wraps = new Wraps();

export async function sendWelcomeEmail(email: string, name: string) {
  return await wraps.emails.send({
    from: 'hello@yourdomain.com',
    to: email,
    subject: `Welcome, ${name}!`,
    html: '<h1>Welcome aboard!</h1>',
  });
}

How OIDC Authentication Works

Your Vercel functions get temporary AWS credentials without storing secrets

Vercel Function
OIDC Token
AWS STS
Validate
Temp Creds
Send via SES

No stored credentials. Tokens refresh automatically. Full audit trail in CloudTrail.

Here's what happens when your Vercel function sends an email:

  1. The SDK requests an OIDC token from Vercel
  2. Vercel returns a signed JWT with your project identity
  3. The SDK calls AWS STS AssumeRoleWithWebIdentity
  4. AWS validates: “Is this token from the trusted Vercel project?”
  5. AWS returns temporary credentials (valid ~1 hour)
  6. The SDK sends email via SES using temporary credentials

The Benefits

  • No stored credentials: No access keys in environment variables that could leak
  • Automatic rotation: Tokens refresh automatically, no manual key rotation
  • Least privilege: The IAM role only has permissions for email operations
  • Audit trail: Every credential grant is logged in CloudTrail

Full SDK Reference

The SDK supports all the features you'd expect from a modern email API

Simple Send

send-simple.ts
import { Wraps } from '@wraps.dev/email';

const wraps = new Wraps();

await wraps.emails.send({
  from: 'hello@yourdomain.com',
  to: 'user@example.com',
  subject: 'Hello!',
  html: '<h1>Hello World</h1>',
});

With All Options

send-with-options.ts
await wraps.emails.send({
  from: 'Team <hello@yourdomain.com>',
  to: ['user1@example.com', 'user2@example.com'],
  cc: 'manager@example.com',
  bcc: 'archive@yourdomain.com',
  replyTo: 'support@yourdomain.com',
  subject: 'Your order is confirmed',
  html: '<h1>Order #1234</h1><p>Thanks for your purchase!</p>',
  text: 'Order #1234 - Thanks for your purchase!',
  headers: {
    'X-Custom-Header': 'custom-value',
  },
});

With Attachments

send-with-attachments.ts
await wraps.emails.send({
  from: 'invoices@yourdomain.com',
  to: 'customer@example.com',
  subject: 'Your Invoice',
  html: '<p>Please find your invoice attached.</p>',
  attachments: [
    {
      filename: 'invoice.pdf',
      content: pdfBuffer,
      contentType: 'application/pdf',
    },
  ],
});

Checking Your Infrastructure Status

See what's deployed to your AWS account at any time

Anytime you want to see what's deployed:

terminal
wraps email status

This shows your AWS Account, Region, Provider, enabled Features (event tracking, email history, dedicated IP), verified Domains, and all deployed Resources.

What About Local Development?

It just works—no changes needed

When developing locally, the SDK falls back to your local AWS credentials (from aws configure or ~/.aws/credentials). No changes needed—it just works.

If you want to test without sending real emails, you can use SES's mailbox simulator addresses:

local-test.ts
await wraps.emails.send({
  from: 'test@yourdomain.com',
  to: 'success@simulator.amazonses.com', // Always succeeds
  subject: 'Test email',
  html: '<p>This is a test.</p>',
});

Real Cost Savings

Stop paying 10x more for the same emails

Monthly Volume
Resend
SendGrid
Wraps + AWS
10,000
$20
$20
~$3Save 85%
50,000
$50
$50
~$7Save 86%
100,000
$90
$90
~$12Save 87%
500,000
$400
$250
~$52Save 79%

AWS costs only: SES sending ($0.10/1K emails) + infrastructure (~$2-5/mo). Wraps free tier included.

Cost Breakdown

AWS (paid directly to AWS):

  • SES: $0.10 per 1,000 emails
  • DynamoDB: ~$0.25 per million reads/writes
  • Lambda: Free tier covers most usage
  • EventBridge: Free

For a typical startup sending 50,000 emails/month, your total AWS cost is about $5. Compare that to $50+ on Resend or SendGrid.

What You Own

Everything deployed by the CLI lives in your AWS account

  • Your SES domain and reputation
  • Your event data in DynamoDB
  • Your Lambda functions and SQS queues
  • Your IAM roles and policies

If you ever stop using Wraps, your email infrastructure keeps working. No lock-in.

Troubleshooting

Common issues and how to resolve them

“OIDC token not available”

This error means you're not running on Vercel. The OIDC flow only works in Vercel's serverless environment. For local development, ensure you have AWS credentials configured via aws configure.

“Domain verification pending”

DNS propagation can take time. Run wraps email domains list to check status. If it has been more than 72 hours, double-check your CNAME records match exactly. Having trouble with SPF lookups? That could be the issue.

“Access Denied” on SES

Your AWS account might still be in SES sandbox mode. Run wraps email status to check. If you need production access, see our guide to escaping the SES sandbox.

TL;DR

The complete setup in four commands

terminal
# 1. Deploy infrastructure
npx @wraps.dev/cli email init

# 2. Add your domain
wraps email domains add -d yourdomain.com

# 3. Install SDK
npm install @wraps.dev/email

# 4. Send emails

Minutes to production email infrastructure. Your AWS, your data, no stored credentials.

Ready to Ship?

Deploy production email infrastructure in minutes.
Your AWS, your data, no stored credentials.

npx @wraps.dev/cli email init