When building a modern web application, sending email might seem simple—until it isn’t.
Most developers start off sending emails directly after form submissions or actions. But as your user base grows, and you need to send thousands of emails—many of them personalised and triggered automatically—this direct approach becomes a bottleneck. Worse, it introduces instability, performance lag, and delivery risks.
In our case, we needed to send bulk personalised emails from a Nuxt 3 app powered by AWS Amplify, and do it in a secure, scalable, and auditable way. We needed full control—while still keeping things developer-friendly.
The Problem
We had several real-world requirements:
-
Support thousands of email sends at once, with custom fields for each user.
-
Ensure emails aren’t lost if a failure occurs (e.g., SMTP errors or timeouts).
-
Log every email with its payload, status, and any failure reasons.
-
Preview emails before they go out.
-
Avoid overloading the client app or delaying responses during email generation.
-
Respect IAM rules and group-level access controls in AWS Amplify.
-
Build this within a Nuxt 3 environment without relying on external automation platforms.
The Stack We Chose and Why
Here’s a breakdown of the stack and the reasoning behind each component.
✅ Nuxt 3
-
Why: It’s our main application framework. Its support for SSR and server routes allows clean separation of API logic and background processing.
-
How it helps: We use Nuxt server routes (
/api
) to trigger email queues and provide admin UIs for template and log management.
✅ AWS Amplify with AppSync & DynamoDB
-
Why: We’re already using Amplify for user management and data storage. Its GraphQL API and real-time subscriptions fit our data access patterns.
-
How it helps:
-
AppSync handles CRUD operations and real-time data for emails and templates.
-
DynamoDB gives us fast, scalable, serverless data persistence.
-
We defined three key models:
-
EmailTemplate
: reusable email structures. -
EmailQueue
: pending jobs. -
EmailLog
: completed or failed messages.
-
-
✅ BullMQ + Redis
-
Why: We needed background job processing with retries, scheduling, and logging.
-
How it helps:
-
BullMQ
manages a queue of email jobs. -
Allows retries, backoff, and concurrency control.
-
Can throttle email sends to prevent spam or quota issues.
-
-
Alternative: We considered Lambda queues but opted for BullMQ for better local control and ease of debugging during development.
✅ Mailgun SMTP + Nodemailer
-
Why Mailgun: Reliable deliverability, good SMTP support, works well with custom domains.
-
Why Nodemailer: A mature, extensible library for sending and formatting emails.
-
How it helps:
-
SMTP ensures authentication, security, and better tracking.
-
Nodemailer lets us send HTML or plain-text versions, include attachments, etc.
-
✅ email-templates (npm)
-
Why: We wanted templating support for emails using layouts and dynamic variables.
-
How it helps:
-
You can define layouts (headers, footers), templates, and use JSON data to merge.
-
Great for onboarding, reminders, announcements, and more.
-
Authentication and Security
We layered in security with AWS Amplify’s auth rules:
-
Only admins (via Cognito groups) can manage templates and view logs.
-
Public or owner rules can enqueue emails but cannot read logs or templates.
-
This keeps the system flexible for both internal use and user-triggered events.
How It All Fits Together (The Flow)
-
Admin creates an email template via the UI or CLI using GraphQL.
-
A user action (or admin) triggers an API route (
/api/email/queue
) that:-
Validates permissions.
-
Fetches the appropriate template.
-
Stores a new
EmailQueue
record in DynamoDB with the template, recipient, and merge data.
-
-
A BullMQ worker polls Redis for pending jobs.
-
It picks up a job, fetches the data + template, renders it using
email-templates
, and sends it viaNodemailer
to Mailgun. -
The result (success/failure, timestamp, etc.) is logged to the
EmailLog
model. -
The admin UI can preview emails before sending and monitor the log for errors.
Why We Avoided Simpler Solutions
We considered:
-
Direct send via SMTP on action: But it blocks the UI and fails silently.
-
Mailgun’s batch API: Lacks fine-grained logging and merging.
-
Using AWS SES: Good option, but we had more experience with Mailgun, and it offers faster setup for our domain.
-
Lambda functions for processing: Doable, but BullMQ offers a better DX and control during development.
What We Didn’t Cover (But You’ll Want to Explore)
-
Template versioning: Add
version
fields or history toEmailTemplate
. -
Rate limiting/throttling: Use BullMQ’s
limiter
option to control how many emails are sent per second. -
Localization: Add
language
support in templates and queue fields. -
UI polish: We built dashboards using Nuxt admin routes and Tailwind.
Resources for Getting Up to Speed
Summary
This architecture:
-
Scales with your application.
-
Separates the concerns of generation, delivery, and audit.
-
Gives you visibility into every email sent.
-
Fits naturally into a Nuxt 3 + Amplify workflow.
If you’re building for users and want to deliver emails reliably without relying on black-box solutions, this architecture offers clarity and control—without too much complexity.
Would you like this published as a blog on your site, or exported as Markdown or PDF for wider distribution?