Oolvay supports AWS SES alongside Resend and Postmark. SES is the lowest-cost option at scale and is the right choice if you are already invested in AWS infrastructure.
SES runs in two modes:
| Mode | When to use |
|---|---|
| Sandbox | Local development and testing. Can only send to verified email addresses. |
| Production | Real deployments with a custom domain. No recipient restrictions after approval. |
If you are using Resend or Postmark, skip this page. See the Email page instead.
Complete all steps in the Infrastructure and Storage pages first. The AWS CLI profile and CDK bootstrap must be in place before running any commands here.
Sandbox mode lets you test SES locally without a domain. AWS restricts sending to verified email addresses only. Both the sender and recipient must be verified in the SES console.
Use sandbox mode when you:
0 / 2,000 characters
Open the AWS Console and navigate to SES → Configuration → Identities.
Click Create identity, choose Email address, and enter the address you want to use for sending and receiving test emails.
AWS will send a verification link to that address. Open the email and click the link.
Open config/emails.ts and set the provider and mode:
export const emailConfig = {
provider: "ses" as "resend" | "postmark" | "ses",
deploySesInfrastructure: false, // Set to true only if you have a domain name handy and are ready with DNS setup info
}
support: {
sender: `Team ${BRANDNAME}`,
email: "you@example.com",
},
security: {
sender: `${BRANDNAME} Security`,
email: "you@example.com",
},
contact: {
sender: `Team ${BRANDNAME}`,
fromEmail: "you@example.com",
toEmail: "you@example.com",
},
docsFeedback: {
sender: `${BRANDNAME} Docs`,
fromEmail: "you@example.com",
toEmail: "you@example.com", // later change to `feedback@${BRANDDOMAIN}`
},
welcome: {
sender: `${EMAILSENDERNAME} from ${BRANDNAME}`,
fromEmail: "you@example.com",
},
subscriptions: {
sender: `${BRANDNAME} Billing`,
fromEmail: "you@example.com",
},
magicLink: {
sender: `${BRANDNAME} Accounts`,
fromEmail: "you@example.com",
expiresInSeconds: 300, // 5 minutes
},In SES sandbox mode, every sender and recipient address used by your application must be a verified SES identity.
SES sandbox accounts can verify multiple email addresses. Verify whichever addresses you want to use for testing, such as a support address, contact address, or magic-link sender.
If you already completed the steps outlined in the Storage page, the infrastructure is already deployed and your .env.local should already contain the required AWS values. Skip directly to Step 6: Test email delivery.
Otherwise, deploy the core infrastructure stack now:
bunx cdk deploy oolvay-infra-dev --app "bun run infra/app.ts" --profile dev-adminThroughout these examples, replace dev-admin with your own AWS CLI profile name if you used a different one during setup.
When the deployment finishes, the terminal prints the stack outputs. Copy those values. You will need them in the next step.
The values you need are:
| Output key | What it is |
|---|---|
| AWS region | The region your bucket was deployed to |
| S3 bucket name | The full name of your S3 bucket |
| AWS access key ID | Runtime app key to be used by your project server |
| AWS secret access key | Runtime app secret to be used by your project server |
| CloudFront URL | Your CloudFront distribution domain |
The AWS access key ID and AWS secret access key are the restricted runtime
keys for the IAM user CDK created. These are not your dev-admin builder
keys. Never put your dev-admin keys in .env.
Open .env.local and add the values from the terminal output.
# AWS Configuration
AWS_REGION=
AWS_S3_BUCKET_NAME=
# AWS Restricted App User Credentials
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
# AWS CloudFront
NEXT_PUBLIC_CLOUDFRONT_URL=Restart your dev server and trigger any email flow, such as a magic link, welcome email, or contact form submission.
bun devAs long as both the sender and recipient addresses involved in the flow are verified in SES, delivery should work immediately.
Production mode sends email from your own domain with no recipient restrictions. It requires a real domain and access to your DNS provider.
You do not need Route53. Any DNS provider works, whether that is Cloudflare, Namecheap, Porkbun, GoDaddy, Route53, or another provider.
Open config/emails.ts and set the provider and mode:
export const emailConfig = {
provider: "ses" as "resend" | "postmark" | "ses",
deploySesInfrastructure: true, // Set to true to provision SES domain infrastructure
}
support: {
sender: `Team ${BRANDNAME}`,
email: "you@example.com",
},
security: {
sender: `${BRANDNAME} Security`,
email: "you@example.com",
},
contact: {
sender: `Team ${BRANDNAME}`,
fromEmail: "you@example.com",
toEmail: "you@example.com",
},
docsFeedback: {
sender: `${BRANDNAME} Docs`,
fromEmail: "you@example.com",
toEmail: "you@example.com", // later change to `feedback@${BRANDDOMAIN}`
},
welcome: {
sender: `${EMAILSENDERNAME} from ${BRANDNAME}`,
fromEmail: "you@example.com",
},
subscriptions: {
sender: `${BRANDNAME} Billing`,
fromEmail: "you@example.com",
},
magicLink: {
sender: `${BRANDNAME} Accounts`,
fromEmail: "you@example.com",
expiresInSeconds: 300, // 5 minutes
},The sender addresses will be updated in Step 6: Use branded sender addresses once your domain is verified and production access is granted.
deploySesInfrastructure must be true for production mode. Also confirm
that BRANDDOMAIN in your constants file contains your real domain. For
example, use acme.com. Do not use localhost or a placeholder.
Run the deploy command with --all to include the SES identity stack alongside the core infrastructure stack.
bunx cdk deploy --all --app "bun run infra/app.ts" --profile dev-adminThis creates:
When the deployment finishes, the terminal prints DNS records that AWS needs you to add to your domain. These typically include:
| Record type | Purpose |
|---|---|
| TXT | Domain ownership verification |
| CNAME (×3) | DKIM signing keys |
Copy these records into your DNS provider exactly as shown. Regardless of which DNS provider you use, log in, find the DNS management panel for your domain, and add each record.
DNS propagation usually takes a few minutes but can take up to several hours. AWS SES will mark the domain as verified automatically once it detects the records.
.env.localIf you completed the Storage setup earlier, these values are already in your .env.local and the SES stack adds no new environment variables. You can skip this step.
If this is your first CDK deployment, the terminal output from the cdk deploy --all command in Step 2: Deploy SES infrastructure will print the following values. Copy them into your .env.local:
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET_NAME=
NEXT_PUBLIC_CLOUDFRONT_URL=By default, new SES accounts are restricted to sandbox mode. This step lifts that restriction so you can send email to any address without limitations.
In the AWS Console, navigate to SES → Get set up.
Locate the Request production access card. If your DNS records from Step 3: Add DNS records have not propagated and been verified by AWS yet, this card will be disabled with a “Domain verification needed” notice. Wait until the card is enabled before proceeding.
Click Request production access and fill out the form. Select Transactional as the Mail type, enter your app URL under Website URL, and describe how your app uses email in the Use case description field, for example, “Authentication emails, billing receipts, and account notifications for a SaaS application.” Check the Acknowledgement box and click Submit request.
Wait for approval. This usually takes less than 24 hours. AWS will notify you by email when access is granted.
Until production access is approved, your SES account remains in sandbox mode even with a verified domain. You can send to verified addresses in the meantime.
Now that your domain is verified and production access is granted, update config/emails.ts with your real sender addresses.
export const emailConfig = {
provider: "ses" as "resend" | "postmark" | "ses",
deploySesInfrastructure: true,
}
support: {
sender: `Team ${
Consider using separate inboxes for different purposes, such as
support@yourdomain.com, billing@yourdomain.com, security@yourdomain.com,
feedback@yourdomain.com, and noreply@yourdomain.com. This makes message
routing, filtering, and team ownership much easier as your application grows.