OneApp Docs
Platform Apps

email

React Email development server for previewing and testing email templates. Hot reload, responsive testing, dark mode support. Interactive environment for designing transactional emails with Resend integration.

Quick Start

Preview email templates in 30 seconds:

pnpm --filter email dev

Opens at localhost:3500. Edit templates, see changes instantly. Skip to Quick Start →

Why email?

Email templates scattered across applications. No live preview for email changes. Manual testing across email clients. Styling inconsistencies in production emails. Duplicated email logic. No shared template components.

email solves this by centralizing all email templates in a React Email development environment with hot reload, responsive preview, and integration with @repo/email for production use.

Production-ready with React Email 5, hot reload, responsive/dark mode testing, Resend integration, shared components, Vitest testing, and HTML export.

Use cases

  • Template Development — Build and preview email templates with hot reload
  • Responsive Testing — Test emails on desktop and mobile viewports
  • Dark Mode — Preview dark mode email support
  • Client Testing — Export HTML to test in email clients (Gmail, Outlook)
  • Production Integration — Templates consumed by @repo/email with Resend

How it works

email uses React Email components to build email templates:

// emails/auth/MagicLink.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Text,
} from "@react-email/components";

interface MagicLinkEmailProps {
  magicLink: string;
  email: string;
}

export default function MagicLinkEmail({
  magicLink = "https://example.com/auth/magic?token=xxx",
  email = "user@example.com",
}: MagicLinkEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Sign in to your account</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Sign in to Your Account</Heading>
          <Text style={text}>Click the link below to sign in as {email}:</Text>
          <Link href={magicLink} style={button}>
            Sign In
          </Link>
        </Container>
      </Body>
    </Html>
  );
}

const main = { backgroundColor: "#f6f9fc", fontFamily: "Arial, sans-serif" };
const container = { backgroundColor: "#ffffff", margin: "0 auto", padding: "40px" };
const h1 = { color: "#1a1a1a", fontSize: "24px", fontWeight: "bold" };
const text = { color: "#4a4a4a", fontSize: "16px", lineHeight: "24px" };
const button = {
  backgroundColor: "#0066ff",
  borderRadius: "4px",
  color: "#ffffff",
  padding: "12px 24px",
  textDecoration: "none",
};

Uses React Email for component-based templates, Resend for production sending, and hot reload for instant preview.

Key features

Hot Reload — Live preview of email template changes

Responsive Testing — Desktop and mobile viewport previews

Dark Mode — Test dark mode email rendering

Resend Integration — Production email sending with Resend API

Shared Components — Reusable header, footer, button components

HTML Export — Export templates as static HTML files

Quick Start

1. Start the email dev server

pnpm --filter email dev

2. Browse templates

Open http://localhost:3500 and browse available email templates.

3. Create a new template

platform/apps/email/emails/notifications/AccountCreated.tsx
import { Html, Head, Preview, Body, Container, Heading, Text } from "@react-email/components";

interface AccountCreatedProps {
  name: string;
}

export default function AccountCreatedEmail({ name = "User" }: AccountCreatedProps) {
  return (
    <Html>
      <Head />
      <Preview>Welcome to OneApp!</Preview>
      <Body style={{ backgroundColor: "#f6f9fc", fontFamily: "Arial, sans-serif" }}>
        <Container style={{ padding: "40px" }}>
          <Heading>Welcome, {name}!</Heading>
          <Text>Your account has been successfully created.</Text>
        </Container>
      </Body>
    </Html>
  );
}

4. Use in production

apps/oneapp-onstage/app/api/auth/signup/route.ts
import { sendAccountCreated } from "@repo/email";

// After user signs up
await sendAccountCreated({
  to: user.email,
  name: user.name
});

That's it! Your email template appears in the dev server with hot reload, and can be sent in production via @repo/email with Resend.

Development Tip

Use default props in templates for realistic preview data: { name = "John Doe" } instead of { name: string }.


Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationplatform/apps/email
Port3500 (development)
FrameworkReact Email 5.0
Integration@repo/email (production email package)
TestingVitest 4.0
DeploymentTemplates bundled with @repo/email
Tech StackReact 19, TypeScript 5, Resend

Project Structure

email/
├── emails/                           # Email templates
│   ├── auth/                         # Authentication emails
│   │   ├── MagicLink.tsx             # Passwordless login link
│   │   ├── PasswordReset.tsx         # Password reset link
│   │   └── EmailVerification.tsx    # Account verification
│   ├── notifications/                # Notification emails
│   │   ├── Welcome.tsx               # Welcome email
│   │   ├── ApiKeyCreated.tsx         # API key notification
│   │   └── AccountCreated.tsx        # Account creation
│   └── transactional/                # Transactional emails
│       ├── OrgInvitation.tsx         # Team invitation
│       ├── Contact.tsx               # Contact form response
│       └── Receipt.tsx               # Purchase receipt
├── components/                       # Shared email components
│   ├── EmailLayout.tsx               # Base layout wrapper
│   ├── Header.tsx                    # Email header
│   ├── Footer.tsx                    # Email footer
│   ├── Button.tsx                    # Call-to-action button
│   └── Logo.tsx                      # Company logo
├── __tests__/                        # Email template tests
│   ├── MagicLink.test.tsx
│   ├── Welcome.test.tsx
│   └── OrgInvitation.test.tsx
├── static/                           # Static assets (images)
│   ├── logo.png
│   └── social-icons/
├── env.ts                            # Environment configuration
├── scripts/                          # Build and export scripts
│   └── export-html.ts                # HTML export script
├── vitest.config.ts                  # Test configuration
└── package.json                      # Dependencies and scripts

Available Templates

Authentication Emails

TemplateFile PathDescriptionTrigger Event
MagicLinkemails/auth/MagicLink.tsxPasswordless login linkSign in request
PasswordResetemails/auth/PasswordReset.tsxPassword reset linkForgot password
EmailVerificationemails/auth/EmailVerification.tsxAccount verification linkRegistration

Notification Emails

TemplateFile PathDescriptionTrigger Event
Welcomeemails/notifications/Welcome.tsxUser onboardingAccount creation
ApiKeyCreatedemails/notifications/ApiKeyCreated.tsxAPI key notificationKey generation
AccountCreatedemails/notifications/AccountCreated.tsxAccount creationSignup completion

Transactional Emails

TemplateFile PathDescriptionTrigger Event
OrgInvitationemails/transactional/OrgInvitation.tsxTeam invitationInvite sent
Contactemails/transactional/Contact.tsxContact form responseForm submission
Receiptemails/transactional/Receipt.tsxPurchase receiptPayment completed

Writing Email Templates

Basic Template Structure

// emails/auth/MagicLink.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Text,
} from "@react-email/components";

interface MagicLinkEmailProps {
  magicLink: string;
  email: string;
}

export default function MagicLinkEmail({
  magicLink = "https://example.com/auth/magic?token=xxx",
  email = "user@example.com",
}: MagicLinkEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Sign in to your account</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Sign in to Your Account</Heading>
          <Text style={text}>
            Click the link below to sign in as {email}:
          </Text>
          <Link href={magicLink} style={button}>
            Sign In
          </Link>
          <Text style={footer}>
            If you didn't request this, you can safely ignore this email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

// Inline styles (required for email clients)
const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif",
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  padding: "40px 20px",
  borderRadius: "8px",
  maxWidth: "600px",
};

const h1 = {
  color: "#1a1a1a",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "0 0 20px",
};

const text = {
  color: "#4a4a4a",
  fontSize: "16px",
  lineHeight: "24px",
  margin: "0 0 20px",
};

const button = {
  backgroundColor: "#0066ff",
  borderRadius: "4px",
  color: "#ffffff",
  display: "inline-block",
  fontSize: "16px",
  fontWeight: "bold",
  padding: "12px 24px",
  textDecoration: "none",
  textAlign: "center" as const,
  margin: "20px 0",
};

const footer = {
  color: "#8a8a8a",
  fontSize: "14px",
  lineHeight: "20px",
  margin: "40px 0 0",
};

Key Points:

  • Use default prop values for preview mode
  • All styles must be inline objects
  • Use as const for CSS values that need literal types
  • Include <Preview> for email preview text
  • Keep layout simple (no flexbox, grid, or advanced CSS)

Using Shared Components

// components/EmailLayout.tsx
import {
  Body,
  Container,
  Head,
  Html,
  Preview,
  Section,
} from "@react-email/components";
import { Header } from "./Header";
import { Footer } from "./Footer";

interface EmailLayoutProps {
  preview: string;
  children: React.ReactNode;
}

export function EmailLayout({ preview, children }: EmailLayoutProps) {
  return (
    <Html>
      <Head />
      <Preview>{preview}</Preview>
      <Body style={main}>
        <Container style={container}>
          <Header />
          <Section style={content}>{children}</Section>
          <Footer />
        </Container>
      </Body>
    </Html>
  );
}

const main = {
  backgroundColor: "#f6f9fc",
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif",
};

const container = {
  backgroundColor: "#ffffff",
  margin: "0 auto",
  maxWidth: "600px",
  borderRadius: "8px",
};

const content = {
  padding: "40px 20px",
};

Reusable Button Component

// components/Button.tsx
import { Link } from "@react-email/components";

interface ButtonProps {
  href: string;
  children: React.ReactNode;
  variant?: "primary" | "secondary";
}

export function Button({ href, children, variant = "primary" }: ButtonProps) {
  const variantStyles = {
    primary: {
      backgroundColor: "#0066ff",
      color: "#ffffff",
    },
    secondary: {
      backgroundColor: "#f0f0f0",
      color: "#1a1a1a",
    },
  };

  return (
    <Link
      href={href}
      style={{
        ...baseStyle,
        ...variantStyles[variant],
      }}
    >
      {children}
    </Link>
  );
}

const baseStyle = {
  display: "inline-block",
  fontSize: "16px",
  fontWeight: "bold",
  padding: "12px 24px",
  borderRadius: "4px",
  textDecoration: "none",
  textAlign: "center" as const,
};

Template with Layout and Components

// emails/notifications/Welcome.tsx
import { Heading, Text } from "@react-email/components";
import { EmailLayout } from "../../components/EmailLayout";
import { Button } from "../../components/Button";

interface WelcomeEmailProps {
  name: string;
  dashboardUrl: string;
}

export default function WelcomeEmail({
  name = "John Doe",
  dashboardUrl = "https://app.example.com/dashboard",
}: WelcomeEmailProps) {
  return (
    <EmailLayout preview={`Welcome to OneApp, ${name}!`}>
      <Heading style={h1}>Welcome, {name}!</Heading>

      <Text style={text}>
        Thank you for joining OneApp. We're excited to have you on board!
      </Text>

      <Text style={text}>
        Get started by exploring your dashboard:
      </Text>

      <Button href={dashboardUrl}>Go to Dashboard</Button>

      <Text style={footer}>
        If you have any questions, feel free to reply to this email.
      </Text>
    </EmailLayout>
  );
}

const h1 = {
  color: "#1a1a1a",
  fontSize: "24px",
  fontWeight: "bold",
  margin: "0 0 20px",
};

const text = {
  color: "#4a4a4a",
  fontSize: "16px",
  lineHeight: "24px",
  margin: "0 0 16px",
};

const footer = {
  color: "#8a8a8a",
  fontSize: "14px",
  lineHeight: "20px",
  marginTop: "40px",
};

Responsive Email Design

Mobile-Friendly Styles

// Use max-width instead of fixed width
const container = {
  margin: "0 auto",
  maxWidth: "600px", // Not width: 600px
  width: "100%",
};

// Use relative units for padding
const section = {
  padding: "20px", // Works on all screen sizes
};

// Stack columns on mobile
import { Column, Row, Section } from "@react-email/components";

<Section>
  <Row>
    <Column style={{ width: "50%", minWidth: "280px" }}>
      Left content
    </Column>
    <Column style={{ width: "50%", minWidth: "280px" }}>
      Right content
    </Column>
  </Row>
</Section>

Dark Mode Support

// Use data attributes for dark mode
const text = {
  color: "#1a1a1a",
  // Add dark mode color via media query
  "@media (prefers-color-scheme: dark)": {
    color: "#ffffff",
  },
};

// Or use separate dark mode styles
export default function EmailWithDarkMode({ name }: Props) {
  return (
    <Html>
      <Head>
        <style>{`
          @media (prefers-color-scheme: dark) {
            .dark-mode-text {
              color: #ffffff !important;
            }
            .dark-mode-bg {
              background-color: #1a1a1a !important;
            }
          }
        `}</style>
      </Head>
      <Body>
        <Text className="dark-mode-text">Content</Text>
      </Body>
    </Html>
  );
}

Testing Email Templates

Unit Tests

// __tests__/MagicLink.test.tsx
import { render } from "@react-email/render";
import { describe, it, expect } from "vitest";
import MagicLinkEmail from "../emails/auth/MagicLink";

describe("MagicLinkEmail", () => {
  it("renders with magic link", async () => {
    const html = await render(
      <MagicLinkEmail
        magicLink="https://example.com/auth?token=abc123"
        email="test@example.com"
      />
    );

    expect(html).toContain("Sign in to Your Account");
    expect(html).toContain("test@example.com");
    expect(html).toContain("https://example.com/auth?token=abc123");
  });

  it("includes preview text", async () => {
    const html = await render(
      <MagicLinkEmail
        magicLink="https://example.com/auth?token=abc123"
        email="test@example.com"
      />
    );

    expect(html).toContain("Sign in to your account");
  });

  it("renders button with correct href", async () => {
    const html = await render(
      <MagicLinkEmail
        magicLink="https://example.com/auth?token=abc123"
        email="test@example.com"
      />
    );

    expect(html).toContain('href="https://example.com/auth?token=abc123"');
  });
});

Running Tests

# Run all email tests
pnpm --filter email test

# Run tests in watch mode
pnpm --filter email test:watch

# Run tests with coverage
pnpm --filter email test:coverage

Integration with @repo/email

Sending Emails in Production

// @repo/email/src/send.ts
import { Resend } from "resend";
import MagicLinkEmail from "./templates/MagicLink";
import WelcomeEmail from "./templates/Welcome";

const resend = new Resend(process.env.RESEND_API_KEY);

export async function sendMagicLink({ to, magicLink }: { to: string; magicLink: string }) {
  const { data, error } = await resend.emails.send({
    from: process.env.EMAIL_FROM || "noreply@yourdomain.com",
    to,
    subject: "Sign in to your account",
    react: MagicLinkEmail({ magicLink, email: to })
  });

  if (error) {
    throw new Error(`Failed to send magic link: ${error.message}`);
  }

  return data;
}

export async function sendWelcome({ to, name, dashboardUrl }: { to: string; name: string; dashboardUrl: string }) {
  const { data, error } = await resend.emails.send({
    from: process.env.EMAIL_FROM || "noreply@yourdomain.com",
    to,
    subject: `Welcome to OneApp, ${name}!`,
    react: WelcomeEmail({ name, dashboardUrl })
  });

  if (error) {
    throw new Error(`Failed to send welcome email: ${error.message}`);
  }

  return data;
}

Using in Applications

// apps/oneapp-onstage/app/api/auth/magic/route.ts
import { sendMagicLink } from "@repo/email";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const { email } = await request.json();

  // Generate magic link token
  const token = generateToken();
  const magicLink = `${process.env.NEXT_PUBLIC_APP_URL}/auth/magic?token=${token}`;

  // Send email
  await sendMagicLink({
    to: email,
    magicLink
  });

  return Response.json({ success: true });
}

HTML Export

Exporting Templates

# Export all templates to static HTML
pnpm --filter email export

# Output: platform/apps/email/out/
# - magic-link.html
# - password-reset.html
# - welcome.html
# - ...

Export Script

// scripts/export-html.ts
import { render } from "@react-email/render";
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import MagicLinkEmail from "../emails/auth/MagicLink";
import WelcomeEmail from "../emails/notifications/Welcome";

async function exportEmails() {
  const outDir = join(__dirname, "../out");
  mkdirSync(outDir, { recursive: true });

  // Export MagicLink
  const magicLinkHtml = await render(
    <MagicLinkEmail
      magicLink="https://example.com/auth?token=PLACEHOLDER"
      email="user@example.com"
    />
  );
  writeFileSync(join(outDir, "magic-link.html"), magicLinkHtml);

  // Export Welcome
  const welcomeHtml = await render(
    <WelcomeEmail
      name="John Doe"
      dashboardUrl="https://example.com/dashboard"
    />
  );
  writeFileSync(join(outDir, "welcome.html"), welcomeHtml);

  console.log("✅ Exported email templates to out/");
}

exportEmails();

Testing in Email Clients

Use exported HTML files to test in email clients:

  1. Open exported HTML in browser
  2. Copy HTML source
  3. Paste into email client test tool (Litmus, Email on Acid)
  4. Test rendering in Gmail, Outlook, Apple Mail, etc.

Environment Variables

# Resend API (for production sending)
RESEND_API_KEY="re_..."
RESEND_TOKEN="re_..."  # Alternative name

# Email Configuration
EMAIL_FROM="noreply@yourdomain.com"
EMAIL_REPLY_TO="support@yourdomain.com"

# React Email Dev Server
REACT_EMAIL_PORT=3500

# Environment
NODE_ENV="development"
VERCEL_ENV="preview"

Best Practices

1. Use Inline Styles

Email clients have very limited CSS support. Always use inline styles:

// ✅ Good - Inline styles
<Text style={{ color: "#333", fontSize: "16px" }}>Content</Text>

// ❌ Bad - CSS classes (won't work in most clients)
<Text className="text-gray-700 text-base">Content</Text>

// ❌ Bad - External stylesheets (not supported)
<link rel="stylesheet" href="styles.css" />

2. Table-Based Layouts

For maximum compatibility, use table-based layouts:

import { Column, Row, Section } from "@react-email/components";

<Section>
  <Row>
    <Column style={{ width: "50%", padding: "10px" }}>
      Left content
    </Column>
    <Column style={{ width: "50%", padding: "10px" }}>
      Right content
    </Column>
  </Row>
</Section>

3. Web-Safe Fonts

Use web-safe fonts with fallbacks:

const fontFamily = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, Helvetica, sans-serif";

// Web-safe fonts:
// - Arial
// - Helvetica
// - Georgia
// - Times New Roman
// - Courier New
// - Verdana

4. Image Hosting

Host images on a CDN with absolute URLs:

import { Img } from "@react-email/components";

// ✅ Good - Absolute URL
<Img
  src="https://cdn.yourdomain.com/logo.png"
  alt="Company Logo"
  width={150}
  height={50}
/>

// ❌ Bad - Relative path (won't work in emails)
<Img src="/logo.png" alt="Logo" />

5. Accessible Emails

Ensure emails are accessible:

// Use semantic HTML
<Heading>Main Title</Heading>
<Text>Body text</Text>

// Add alt text to images
<Img src="..." alt="Descriptive alt text" />

// Use sufficient color contrast
const text = { color: "#333" }; // Good contrast
const background = { backgroundColor: "#fff" };

// Use descriptive link text
<Link href="...">View your dashboard</Link> // ✅ Good
<Link href="...">Click here</Link> // ❌ Bad

6. Preheader Text

Use <Preview> for preheader text (shows in inbox preview):

<Preview>Sign in to your account - Magic link inside</Preview>

Best practices:

  • Keep to 40-100 characters
  • Make it actionable
  • Don't duplicate the subject line

Scripts Reference

{
  "scripts": {
    "dev": "email dev --port 3500",
    "build": "email build",
    "export": "email export",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint emails components",
    "typecheck": "tsc --noEmit"
  }
}

Command Descriptions:

  • dev — Start hot-reload dev server
  • build — Build email templates
  • export — Export templates as static HTML
  • test — Run Vitest tests
  • test:watch — Run tests in watch mode
  • test:coverage — Run tests with coverage report
  • lint — Lint email templates
  • typecheck — Type check TypeScript

Troubleshooting

Styles Not Rendering

Issue: Styles not appearing in email preview.

Solution: Ensure all styles are inline objects, not classes:

// ✅ Correct
<Text style={{ color: "#333" }}>Content</Text>

// ❌ Incorrect
<Text className="text-gray-700">Content</Text>

Images Not Loading

Issue: Images don't load in email clients.

Solution: Use absolute URLs and ensure images are hosted on a public CDN:

<Img
  src="https://cdn.yourdomain.com/logo.png" // Absolute URL
  alt="Logo"
  width={150}
  height={50}
/>

Hot Reload Not Working

Issue: Changes not reflecting in preview.

Solution: Restart the dev server:

pnpm --filter email dev

Resend API Errors

Issue: Emails failing to send with Resend.

Solution: Verify API key and sender domain:

# Check environment variables
echo $RESEND_API_KEY
echo $EMAIL_FROM

# Verify domain in Resend dashboard:
# https://resend.com/domains
  • @repo/email — Production email package
  • oneapp-onstage — Application using email templates
  • Resend — Email sending service

On this page