Install
$ agentstack add skill-mrlynn-claude-skills-mongodb-email-system ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
mongodb-email-system
Trigger
Use this skill when adding transactional email to a Next.js + MongoDB app. Covers SMTP transport, DB-backed templates with variable interpolation, hardcoded fallbacks, template seeding, admin CRUD, preview, and test-send.
> Every app needs email eventually, and every time it's a mess. DB-backed templates with hardcoded fallbacks means the app never sends a blank email even if someone deletes a template. — ML
Overview
Every DevRel project that touches users needs transactional email: verification, magic links, event confirmations, feedback requests, partner invitations. This skill provides a complete email system with DB-backed templates that can be edited by admins, with hardcoded fallback templates for reliability.
How to Use
Quick Start
Invoke with /mongodb-email-system or let Claude auto-activate when adding transactional email.
Python Tools
scripts/template_variable_checker.py— Parse email templates and report missing/unused variables
Reference Docs
references/template-catalog.md— All 9 built-in templates with variables and previews
Templates & Samples
assets/sample-email-template.html— Starter HTML email with MongoDB brandingassets/expected_template_output.json— Sample EmailTemplate document
Architecture Decisions
- DB-backed templates with hardcoded fallbacks: Templates live in MongoDB for admin editing, but if the DB is down or a template hasn't been seeded yet, hardcoded functions take over. This makes email delivery resilient.
- Lazy seeding: Templates are seeded to the DB on first miss, not on app startup. This avoids migration scripts and works in serverless.
- Singleton transporter: Nodemailer's SMTP transporter is reused across requests to avoid reconnection overhead.
- Fire-and-forget sends: Non-critical emails (2FA codes, notifications) use
.catch(() => {})so failures don't block the main flow. - Handlebars-style interpolation: Templates use
{{variable}}and{{#if variable}}...{{/if}}— simple enough to not need a full template engine dependency.
File Structure
src/lib/
├── email/
│ ├── email-service.ts # SMTP singleton + sendEmail()
│ ├── template-renderer.ts # DB lookup + interpolation + fallback
│ ├── templates.ts # Hardcoded fallback templates
│ └── seed-email-templates.ts # DB seeder (upsert)
└── db/models/
└── EmailTemplate.ts # Template model
Code Patterns
Pattern 1: SMTP Singleton Transport
// src/lib/email/email-service.ts
import nodemailer from "nodemailer";
import type { Transporter } from "nodemailer";
interface EmailOptions {
to: string;
subject: string;
html: string;
text?: string;
}
let transporter: Transporter | null = null;
function getTransporter(): Transporter | null {
if (transporter) return transporter;
const host = process.env.SMTP_HOST;
const port = parseInt(process.env.SMTP_PORT || "587", 10);
const user = process.env.SMTP_USER;
const pass = process.env.SMTP_PASS;
if (!host || !user || !pass) {
console.warn("Email service: SMTP not configured. Emails will not be sent.");
return null;
}
transporter = nodemailer.createTransport({
host,
port,
secure: port === 465,
auth: { user, pass },
});
return transporter;
}
export async function sendEmail(options: EmailOptions): Promise {
const transport = getTransporter();
if (!transport) return false;
const from = process.env.EMAIL_FROM || "App ";
try {
await transport.sendMail({
from,
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
});
return true;
} catch (error) {
console.error("Email service: Failed to send:", error);
return false;
}
}
Pattern 2: Template Renderer with DB + Fallback
// src/lib/email/template-renderer.ts
import { connectToDatabase } from "@/lib/db/connection";
import { EmailTemplateModel } from "@/lib/db/models/EmailTemplate";
import { seedEmailTemplates } from "./seed-email-templates";
import * as fallbackTemplates from "./templates";
let seeded = false;
function interpolate(template: string, variables: Record): string {
// Handle {{#if variable}}...{{/if}} blocks
let result = template.replace(
/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
(_match, varName, content) => (variables[varName] ? content : "")
);
// Replace {{variable}} placeholders
result = result.replace(/\{\{(\w+)\}\}/g, (_match, varName) => variables[varName] ?? "");
return result;
}
export async function renderEmailTemplate(
key: string,
variables: Record
): Promise {
try {
await connectToDatabase();
let template = await EmailTemplateModel.findOne({ key }).lean();
if (!template && !seeded) {
await seedEmailTemplates();
seeded = true;
template = await EmailTemplateModel.findOne({ key }).lean();
}
if (template) {
return {
subject: interpolate(template.subject, variables),
html: interpolate(template.htmlBody, variables),
text: interpolate(template.textBody, variables),
};
}
} catch (error) {
console.error(`Template renderer: DB lookup failed for "${key}":`, error);
}
return renderFallback(key, variables);
}
function renderFallback(key: string, vars: Record): { subject: string; html: string; text: string } {
switch (key) {
case "magic_link":
return fallbackTemplates.magicLinkEmail(vars.userName || "there", vars.url || "");
case "email_verification":
return fallbackTemplates.emailVerificationEmail(vars.userName || "there", vars.verificationUrl || "");
default:
return {
subject: "Notification",
html: `${vars.message || "You have a new notification."}`,
text: vars.message || "You have a new notification.",
};
}
}
Pattern 3: EmailTemplate Model
// src/lib/db/models/EmailTemplate.ts
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IEmailTemplateVariable {
name: string;
required: boolean;
description: string;
example: string;
}
export interface IEmailTemplate extends Document {
key: string;
name: string;
category: "auth" | "event" | "partner" | "notification";
description: string;
subject: string;
htmlBody: string;
textBody: string;
variables: IEmailTemplateVariable[];
isBuiltIn: boolean;
updatedBy?: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const EmailTemplateSchema = new Schema(
{
key: { type: String, required: true, unique: true },
name: { type: String, required: true },
category: { type: String, enum: ["auth", "event", "partner", "notification"], required: true },
description: { type: String, default: "" },
subject: { type: String, required: true },
htmlBody: { type: String, required: true },
textBody: { type: String, required: true },
variables: [{
name: { type: String, required: true },
required: { type: Boolean, default: true },
description: { type: String, default: "" },
example: { type: String, default: "" },
}],
isBuiltIn: { type: Boolean, default: false },
updatedBy: { type: Schema.Types.ObjectId, ref: "User" },
},
{ timestamps: true }
);
EmailTemplateSchema.index({ key: 1 }, { unique: true });
EmailTemplateSchema.index({ category: 1 });
export const EmailTemplateModel =
mongoose.models.EmailTemplate || mongoose.model("EmailTemplate", EmailTemplateSchema);
Pattern 4: Hardcoded Email Templates with Brand Layout
// src/lib/email/templates.ts
const brandColor = "#00684A"; // MongoDB Forest Green
const bgColor = "#f5f5f5";
function layout(content: string): string {
return `
Your App Name
${content}
This is an automated message.
`;
}
export function magicLinkEmail(name: string, url: string) {
return {
subject: "Sign in to Your App",
html: layout(`
Hi ${name},
Click below to sign in. This link expires in 15 minutes.
Sign In
If you didn't request this, ignore this email.
`),
text: `Hi ${name},\n\nSign in: ${url}\n\nExpires in 15 minutes.`,
};
}
// Additional template functions follow the same pattern:
// twoFactorCodeEmail, emailVerificationEmail, feedbackRequestEmail,
// notificationEmail, registrationConfirmationEmail, partnerInviteEmail,
// partnerAccessApprovedEmail, partnerAccessDeniedEmail
Pattern 5: Admin Template Routes
src/app/api/admin/email-templates/
├── route.ts # GET (list), POST (create)
└── [id]/
├── route.ts # GET, PATCH, DELETE
├── preview/route.ts # POST — render without sending
└── test-send/route.ts # POST — send test email
Preview route renders the template with provided variables and returns { subject, html, text } without sending.
Test-send route renders and sends to the admin's email (or a custom recipient), prepending [TEST] to the subject. Auto-fills missing variables with example values from the template's variables array.
Built-in Template Keys
| Key | Category | Variables | |-----|----------|-----------| | magic_link | auth | userName, url | | two_factor_code | auth | userName, code | | email_verification | auth | userName, verificationUrl | | feedback_request | event | recipientName, eventName, formUrl | | notification | notification | userName, title, message, actionUrl | | registration_confirmation | event | userName, eventName, eventDate, eventLocation, dashboardUrl | | partner_invite | partner | userName, companyName, url | | partner_access_approved | partner | userName, companyName, portalUrl | | partner_access_denied | partner | userName, notes |
Environment Variables
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=your-api-key
EMAIL_FROM="Your App "
Dependencies
npm install nodemailer
npm install -D @types/nodemailer
Common Pitfalls
- Don't block on non-critical email sends. Use
sendEmail({...}).catch(() => {})for 2FA codes, notifications, etc. - Don't hardcode brand colors in individual templates. Use the
layout()wrapper withbrandColorconstant so updates propagate. - Don't forget the text fallback. Every template must provide both HTML and plain text for email clients that don't render HTML.
- Don't edit built-in templates via the API without the
isBuiltInguard. Admin CRUD should prevent deletion of built-in templates. - Use
select: falsepattern for template variables with sensitive defaults (API keys, tokens).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mrlynn
- Source: mrlynn/claude-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.