React Email15 min read

React Email Preview Environment Patterns: Dev, Staging, and Production Preview Servers

Build preview environments for React Email that scale from solo dev to team collaboration. Local preview servers, staging galleries, CI-deployed previews, and production-safe admin routes.

R

React Emails Pro

March 3, 2026

You can't iterate on email design by deploying to production and checking your inbox. You need previews.

But most React Email preview setups stop at npm run dev — which works fine solo, but breaks down the moment you need teammates to review, QA to test across clients, or a safe way to preview templates in staging without sending real emails.

This guide covers five preview environment patterns: local dev servers, team preview endpoints, staging previews, CI-deployed previews, and production read-only preview routes. Pick the patterns that match your scale.

Pattern 1: Local Preview Server (React Email Dev Mode)

Start here. React Email ships with a built-in preview server that renders all templates with hot reload. Perfect for solo development.

terminal
# Install React Email
npm install react-email @react-email/components

# Add to package.json scripts
{
  "email:dev": "email dev"
}

# Start preview server
npm run email:dev

This starts a preview server at localhost:3000 with a sidebar listing all your email templates. Click any template to see it rendered with preview data.

Pro tip: Create a preview-data.ts file that exports realistic data fixtures for each template. Makes iteration way faster than hardcoding props in each file.

File Structure That Works

project structure
emails/
├── templates/
│   ├── welcome.tsx
│   ├── password-reset.tsx
│   └── invoice.tsx
├── preview-data/
│   ├── welcome.ts
│   ├── password-reset.ts
│   └── invoice.ts
└── components/
    └── base-layout.tsx

Each template gets a matching preview data file. React Email automatically picks up the exported default.

emails/preview-data/welcome.ts
export default {
  userName: "Alex Morgan",
  ctaUrl: "https://app.yourproduct.com/onboarding",
  trialDays: 14,
  supportEmail: "help@yourproduct.com",
};

Pattern 2: Team Preview Endpoint (for Design Review)

Local previews don't help teammates who aren't running the dev server. Add a /api/email-preview route that renders templates on-demand with query params.

app/api/email-preview/route.ts
import { render } from "@react-email/render";
import { NextRequest, NextResponse } from "next/server";
import WelcomeEmail from "@/emails/templates/welcome";
import PasswordResetEmail from "@/emails/templates/password-reset";
import previewData from "@/emails/preview-data";

// Only allow in non-production
if (process.env.NODE_ENV === "production") {
  throw new Error("Preview route should not exist in production");
}

const TEMPLATES = {
  welcome: WelcomeEmail,
  "password-reset": PasswordResetEmail,
  // Add more...
};

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const template = searchParams.get("template");
  const variant = searchParams.get("variant") || "default";

  if (!template || !TEMPLATES[template as keyof typeof TEMPLATES]) {
    return NextResponse.json(
      { error: "Invalid template" },
      { status: 400 }
    );
  }

  const TemplateComponent = TEMPLATES[template as keyof typeof TEMPLATES];
  const props = previewData[template]?.[variant] || previewData[template];

  const html = render(<TemplateComponent {...props} />);

  return new NextResponse(html, {
    headers: { "Content-Type": "text/html" },
  });
}

Now teammates can preview templates at:
/api/email-preview?template=welcome

Security: This route should only exist in dev and staging. Add environment checks or use Next.js route conditions to prevent accidental production exposure.

Pattern 3: Staging Preview Gallery (QA-Friendly)

Build a dedicated preview gallery page that lists all templates with variants. Great for QA teams testing across different email clients.

app/email-preview/page.tsx
import { getAllTemplates } from "@/lib/email-registry";

export default function EmailPreviewPage() {
  const templates = getAllTemplates();

  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold mb-6">Email Preview Gallery</h1>
      
      <div className="grid gap-4">
        {templates.map((template) => (
          <div key={template.slug} className="border rounded-lg p-4">
            <h2 className="font-semibold">{template.name}</h2>
            <p className="text-sm text-gray-600 mb-3">{template.description}</p>
            
            <div className="flex gap-2">
              <a
                href={`/api/email-preview?template=${template.slug}`}
                target="_blank"
                className="text-blue-600 hover:underline text-sm"
              >
                Preview Default
              </a>
              
              {template.variants?.map((variant) => (
                <a
                  key={variant}
                  href={`/api/email-preview?template=${template.slug}&variant=${variant}`}
                  target="_blank"
                  className="text-blue-600 hover:underline text-sm"
                >
                  Preview {variant}
                </a>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Add a simple template registry to make this work:

lib/email-registry.ts
export const EMAIL_TEMPLATES = [
  {
    slug: "welcome",
    name: "Welcome Email",
    description: "Sent after user signs up",
    variants: ["with-trial", "immediate-activation"],
  },
  {
    slug: "password-reset",
    name: "Password Reset",
    description: "Security email for password resets",
    variants: ["standard", "suspicious-activity"],
  },
  // Add more...
];

export function getAllTemplates() {
  return EMAIL_TEMPLATES;
}

Pattern 4: CI-Deployed Previews (for PR Reviews)

Deploy preview environments for every pull request. Lets designers and stakeholders review email changes without pulling code locally.

Works great with:

  • Vercel: Automatic preview deployments for every PR
  • Netlify: Deploy previews with unique URLs
  • GitHub Actions: Custom preview deployment workflow
.github/workflows/preview.yml
name: Deploy Email Previews

on:
  pull_request:
    paths:
      - 'emails/**'
      - 'components/**'

jobs:
  deploy-preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build email previews
        run: npm run email:build
      
      - name: Deploy to preview environment
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
        run: |
          npx vercel --token=$VERCEL_TOKEN --yes
          
      - name: Comment preview URL on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '📧 Email preview deployed: https://preview-${{ github.event.pull_request.number }}.yourapp.com/email-preview'
            })
Add a comment bot that posts direct links to changed templates. Makes review way faster than "go to the preview URL and find the template yourself."

Pattern 5: Production-Safe Preview Route (Read-Only Admin)

Sometimes you need to preview emails in production — to debug issues, show stakeholders what was sent, or test with real data. But you can't just expose a public preview endpoint.

Add an admin-only, read-only preview route with proper authentication:

app/admin/email-preview/route.ts
import { render } from "@react-email/render";
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

export async function GET(request: NextRequest) {
  // 1. Require authentication
  const session = await getServerSession(authOptions);
  if (!session?.user?.isAdmin) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 2. Rate limit
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const rateLimitOk = await checkRateLimit(ip, { max: 10, window: 60 });
  if (!rateLimitOk) {
    return NextResponse.json(
      { error: "Rate limit exceeded" },
      { status: 429 }
    );
  }

  // 3. Render template with sanitized props
  const searchParams = request.nextUrl.searchParams;
  const template = searchParams.get("template");
  const dataId = searchParams.get("dataId");

  // Fetch safe preview data (never expose sensitive user data)
  const safeProps = await getPreviewData(template, dataId);

  const html = render(getTemplateComponent(template, safeProps));

  return new NextResponse(html, {
    headers: {
      "Content-Type": "text/html",
      "X-Robots-Tag": "noindex, nofollow",
      "Cache-Control": "no-store, max-age=0",
    },
  });
}

Security checklist for production previews:

  • ✅ Require admin authentication
  • ✅ Rate limit aggressively
  • ✅ Never expose real user data (use sanitized fixtures)
  • ✅ Add noindex headers
  • ✅ Log all access for audit trail
  • ✅ Consider IP allowlisting for extra paranoia

Bonus: Cross-Client Preview Testing

Previews in a browser aren't enough. Gmail, Outlook, and Apple Mail render HTML differently. Use these tools to test real client rendering:

  • Email on Acid: Automated cross-client screenshots
  • Litmus: Preview + spam testing
  • Mailtrap: Safe inbox for staging email testing
  • Playwright: Automated visual regression tests
tests/email-visual.test.ts
import { test, expect } from "@playwright/test";

test("Welcome email renders correctly", async ({ page }) => {
  await page.goto("http://localhost:3000/api/email-preview?template=welcome");
  
  // Wait for email to render
  await page.waitForSelector("body");
  
  // Take screenshot for visual regression
  await expect(page).toHaveScreenshot("welcome-email.png", {
    fullPage: true,
  });
  
  // Verify critical elements
  await expect(page.locator("h1")).toContainText("Welcome");
  await expect(page.locator('a[href*="onboarding"]')).toBeVisible();
});
Run visual regression tests in CI. Catches accidental layout breaks before they reach production.

Which Patterns to Use?

Solo developer / early startup:

  • ✅ Pattern 1 (local preview)
  • ✅ Pattern 2 (team preview API)

Small team (2-10 people):

  • ✅ Pattern 1 (local preview)
  • ✅ Pattern 3 (staging gallery)
  • ✅ Pattern 4 (CI previews for PRs)

Production SaaS with compliance requirements:

  • ✅ All patterns
  • ✅ Pattern 5 (production admin previews)
  • ✅ Automated cross-client testing
  • ✅ Audit logging for preview access

Common Mistakes

1) Exposing preview routes in production

Don't leave /api/email-preview accessible in production without auth. Attackers can use it to:

  • Enumerate email templates
  • Discover internal URLs
  • Test injection attacks

Fix: Add environment checks or move preview routes to admin-only paths.


2) Using production data in preview fixtures

Never pull real user data into preview environments. Use sanitized fixtures or synthetic data.

lib/safe-preview-data.ts
// ❌ Bad: Exposes real user data
const props = await db.user.findFirst();

// ✅ Good: Synthetic preview data
const props = {
  userName: "Alex Morgan (Test User)",
  email: "preview@example.com",
  ctaUrl: "https://staging.yourapp.com/test",
};

3) No variant support

Most templates have edge cases: long names, missing data, different states. Preview all variants, not just the happy path.

emails/preview-data/invoice.ts
export default {
  default: {
    invoiceNumber: "INV-2024-001",
    lineItems: [
      { name: "Pro Plan", amount: 49.00 },
      { name: "Additional seats (2)", amount: 20.00 },
    ],
    total: 69.00,
  },
  
  singleItem: {
    invoiceNumber: "INV-2024-002",
    lineItems: [
      { name: "Starter Plan", amount: 19.00 },
    ],
    total: 19.00,
  },
  
  longNames: {
    invoiceNumber: "INV-2024-003",
    lineItems: [
      { 
        name: "Enterprise Plan with Additional Features and Custom Integration Support", 
        amount: 499.00 
      },
    ],
    total: 499.00,
  },
};

Implementation Checklist

  • ✅ Set up React Email dev server for local iteration
  • ✅ Create preview data fixtures for all templates
  • ✅ Add team preview API route (dev/staging only)
  • ✅ Build preview gallery page for QA
  • ✅ Set up CI preview deployments for PRs
  • ✅ Add production admin preview route (if needed)
  • ✅ Test across Gmail, Outlook, Apple Mail
  • ✅ Add visual regression tests
Want production-ready email templates with preview data built in? Check out our SaaS template library — all templates include default and edge case variants.

Good preview environments turn email iteration from "deploy and pray" into a confident, collaborative workflow. Start with local previews, add team/staging patterns as you scale, and keep production previews locked down.

Production-ready templates

Pick from 9 template packs built with React Email. One-time purchase, lifetime updates, tested across every major email client.

Browse all templates