AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Mongodb Email System

skill-mrlynn-claude-skills-mongodb-email-system · by mrlynn

Add transactional email with DB-backed templates, variable interpolation, hardcoded fallbacks, SMTP singleton transport, and admin template management to a Next.js + MongoDB app

No reviews yet
0 installs
16 views
0.0% view→install

Install

$ agentstack add skill-mrlynn-claude-skills-mongodb-email-system

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-mrlynn-claude-skills-mongodb-email-system)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Mongodb Email System? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 branding
  • assets/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 with brandColor constant 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 isBuiltIn guard. Admin CRUD should prevent deletion of built-in templates.
  • Use select: false pattern 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.