+ ${categoryHtml}
+
+`,
+ { headers: { 'Content-Type': 'text/html' } }
+ )
+ }
+
+ if (!(template in emailTemplates)) {
+ return NextResponse.json({ error: `Unknown template: ${template}` }, { status: 400 })
+ }
+
+ const html = await emailTemplates[template]()
+
+ return new NextResponse(html, {
+ headers: { 'Content-Type': 'text/html' },
+ })
+}
diff --git a/apps/sim/app/api/help/route.ts b/apps/sim/app/api/help/route.ts
index ca3d040c2a..676397d2d2 100644
--- a/apps/sim/app/api/help/route.ts
+++ b/apps/sim/app/api/help/route.ts
@@ -118,7 +118,6 @@ ${message}
// Send confirmation email to the user
try {
const confirmationHtml = await renderHelpConfirmationEmail(
- email,
type as 'bug' | 'feedback' | 'feature_request' | 'other',
images.length
)
diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts
index 46cdabea9d..f72705e90e 100644
--- a/apps/sim/app/api/organizations/[id]/invitations/route.ts
+++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts
@@ -16,7 +16,7 @@ import {
getEmailSubject,
renderBatchInvitationEmail,
renderInvitationEmail,
-} from '@/components/emails/render-email'
+} from '@/components/emails'
import { getSession } from '@/lib/auth'
import {
validateBulkInvitations,
@@ -376,8 +376,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
- `${getBaseUrl()}/invite/${orgInvitation.id}`,
- email
+ `${getBaseUrl()}/invite/${orgInvitation.id}`
)
emailResult = await sendEmail({
diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts
index 4ada7c2ba8..eb3f4b0cda 100644
--- a/apps/sim/app/api/organizations/[id]/members/route.ts
+++ b/apps/sim/app/api/organizations/[id]/members/route.ts
@@ -4,7 +4,7 @@ import { invitation, member, organization, user, userStats } from '@sim/db/schem
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
-import { getEmailSubject, renderInvitationEmail } from '@/components/emails/render-email'
+import { getEmailSubject, renderInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getUserUsageData } from '@/lib/billing/core/usage'
import { validateSeatAvailability } from '@/lib/billing/validation/seat-management'
@@ -260,8 +260,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const emailHtml = await renderInvitationEmail(
inviter[0]?.name || 'Someone',
organizationEntry[0]?.name || 'organization',
- `${getBaseUrl()}/invite/organization?id=${invitationId}`,
- normalizedEmail
+ `${getBaseUrl()}/invite/organization?id=${invitationId}`
)
const emailResult = await sendEmail({
diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
index 0d427f1779..4d4ac7928f 100644
--- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.ts
@@ -11,7 +11,7 @@ import {
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
-import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
+import { WorkspaceInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts
index 96370bf63d..b47e7bf362 100644
--- a/apps/sim/app/api/workspaces/invitations/route.test.ts
+++ b/apps/sim/app/api/workspaces/invitations/route.test.ts
@@ -87,14 +87,10 @@ describe('Workspace Invitations API Route', () => {
WorkspaceInvitationEmail: vi.fn(),
}))
- vi.doMock('@/lib/core/config/env', () => ({
- env: {
- RESEND_API_KEY: 'test-resend-key',
- NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
- FROM_EMAIL_ADDRESS: 'Sim ',
- EMAIL_DOMAIN: 'test.sim.ai',
- },
- }))
+ vi.doMock('@/lib/core/config/env', async () => {
+ const { createEnvMock } = await import('@sim/testing')
+ return createEnvMock()
+ })
vi.doMock('@/lib/core/utils/urls', () => ({
getEmailDomain: vi.fn().mockReturnValue('sim.ai'),
diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts
index 6ad6285b35..19ee610878 100644
--- a/apps/sim/app/api/workspaces/invitations/route.ts
+++ b/apps/sim/app/api/workspaces/invitations/route.ts
@@ -12,7 +12,7 @@ import {
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
-import { WorkspaceInvitationEmail } from '@/components/emails/workspace-invitation'
+import { WorkspaceInvitationEmail } from '@/components/emails'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { sendEmail } from '@/lib/messaging/email/mailer'
diff --git a/apps/sim/components/emails/_styles/base.ts b/apps/sim/components/emails/_styles/base.ts
new file mode 100644
index 0000000000..844ac1c55f
--- /dev/null
+++ b/apps/sim/components/emails/_styles/base.ts
@@ -0,0 +1,246 @@
+/**
+ * Base styles for all email templates.
+ * Colors are derived from globals.css light mode tokens.
+ */
+
+/** Color tokens from globals.css (light mode) */
+export const colors = {
+ /** Main canvas background */
+ bgOuter: '#F7F9FC',
+ /** Card/container background - pure white */
+ bgCard: '#ffffff',
+ /** Primary text color */
+ textPrimary: '#2d2d2d',
+ /** Secondary text color */
+ textSecondary: '#404040',
+ /** Tertiary text color */
+ textTertiary: '#5c5c5c',
+ /** Muted text (footer) */
+ textMuted: '#737373',
+ /** Brand primary - purple */
+ brandPrimary: '#6f3dfa',
+ /** Brand tertiary - green (matches Run/Deploy buttons) */
+ brandTertiary: '#32bd7e',
+ /** Border/divider color */
+ divider: '#ededed',
+ /** Footer background */
+ footerBg: '#F7F9FC',
+}
+
+/** Typography settings */
+export const typography = {
+ fontFamily: "-apple-system, 'SF Pro Display', 'SF Pro Text', 'Helvetica', sans-serif",
+ fontSize: {
+ body: '16px',
+ small: '14px',
+ caption: '12px',
+ },
+ lineHeight: {
+ body: '24px',
+ caption: '20px',
+ },
+}
+
+/** Spacing values */
+export const spacing = {
+ containerWidth: 600,
+ gutter: 40,
+ sectionGap: 20,
+ paragraphGap: 12,
+ /** Logo width in pixels */
+ logoWidth: 90,
+}
+
+export const baseStyles = {
+ fontFamily: typography.fontFamily,
+
+ /** Main body wrapper with outer background */
+ main: {
+ backgroundColor: colors.bgOuter,
+ fontFamily: typography.fontFamily,
+ padding: '32px 0',
+ },
+
+ /** Center wrapper for email content */
+ wrapper: {
+ maxWidth: `${spacing.containerWidth}px`,
+ margin: '0 auto',
+ },
+
+ /** Main card container with rounded corners */
+ container: {
+ maxWidth: `${spacing.containerWidth}px`,
+ margin: '0 auto',
+ backgroundColor: colors.bgCard,
+ borderRadius: '16px',
+ overflow: 'hidden',
+ },
+
+ /** Header section with logo */
+ header: {
+ padding: `32px ${spacing.gutter}px 16px ${spacing.gutter}px`,
+ textAlign: 'left' as const,
+ },
+
+ /** Main content area with horizontal padding */
+ content: {
+ padding: `0 ${spacing.gutter}px 32px ${spacing.gutter}px`,
+ },
+
+ /** Standard paragraph text */
+ paragraph: {
+ fontSize: typography.fontSize.body,
+ lineHeight: typography.lineHeight.body,
+ color: colors.textSecondary,
+ fontWeight: 400,
+ fontFamily: typography.fontFamily,
+ margin: `${spacing.paragraphGap}px 0`,
+ },
+
+ /** Bold label text (e.g., "Platform:", "Time:") */
+ label: {
+ fontSize: typography.fontSize.body,
+ lineHeight: typography.lineHeight.body,
+ color: colors.textSecondary,
+ fontWeight: 'bold' as const,
+ fontFamily: typography.fontFamily,
+ margin: 0,
+ display: 'inline',
+ },
+
+ /** Primary CTA button - matches app tertiary button style */
+ button: {
+ display: 'inline-block',
+ backgroundColor: colors.brandTertiary,
+ color: '#ffffff',
+ fontWeight: 500,
+ fontSize: '14px',
+ padding: '6px 12px',
+ borderRadius: '5px',
+ textDecoration: 'none',
+ textAlign: 'center' as const,
+ margin: '4px 0',
+ fontFamily: typography.fontFamily,
+ },
+
+ /** Link text style */
+ link: {
+ color: colors.brandTertiary,
+ fontWeight: 'bold' as const,
+ textDecoration: 'none',
+ },
+
+ /** Horizontal divider */
+ divider: {
+ borderTop: `1px solid ${colors.divider}`,
+ margin: `16px 0`,
+ },
+
+ /** Footer container (inside gray area below card) */
+ footer: {
+ maxWidth: `${spacing.containerWidth}px`,
+ margin: '0 auto',
+ padding: `32px ${spacing.gutter}px`,
+ textAlign: 'left' as const,
+ },
+
+ /** Footer text style */
+ footerText: {
+ fontSize: typography.fontSize.caption,
+ lineHeight: typography.lineHeight.caption,
+ color: colors.textMuted,
+ fontFamily: typography.fontFamily,
+ margin: '0 0 10px 0',
+ },
+
+ /** Code/OTP container */
+ codeContainer: {
+ margin: '12px 0',
+ padding: '12px 16px',
+ backgroundColor: '#f8f9fa',
+ borderRadius: '6px',
+ border: `1px solid ${colors.divider}`,
+ textAlign: 'center' as const,
+ },
+
+ /** Code/OTP text */
+ code: {
+ fontSize: '24px',
+ fontWeight: 'bold' as const,
+ letterSpacing: '3px',
+ color: colors.textPrimary,
+ fontFamily: typography.fontFamily,
+ margin: 0,
+ },
+
+ /** Highlighted info box (e.g., "What you get with Pro") */
+ infoBox: {
+ backgroundColor: colors.bgOuter,
+ padding: '16px 18px',
+ borderRadius: '6px',
+ margin: '16px 0',
+ },
+
+ /** Info box title */
+ infoBoxTitle: {
+ fontSize: typography.fontSize.body,
+ lineHeight: typography.lineHeight.body,
+ fontWeight: 600,
+ color: colors.textPrimary,
+ fontFamily: typography.fontFamily,
+ margin: '0 0 8px 0',
+ },
+
+ /** Info box list content */
+ infoBoxList: {
+ fontSize: typography.fontSize.body,
+ lineHeight: '1.6',
+ color: colors.textSecondary,
+ fontFamily: typography.fontFamily,
+ margin: 0,
+ },
+
+ /** Section borders - decorative accent line */
+ sectionsBorders: {
+ width: '100%',
+ display: 'flex',
+ },
+
+ sectionBorder: {
+ borderBottom: `1px solid ${colors.divider}`,
+ width: '249px',
+ },
+
+ sectionCenter: {
+ borderBottom: `1px solid ${colors.brandTertiary}`,
+ width: '102px',
+ },
+
+ /** Spacer row for vertical spacing in tables */
+ spacer: {
+ border: 0,
+ margin: 0,
+ padding: 0,
+ fontSize: '1px',
+ lineHeight: '1px',
+ },
+
+ /** Gutter cell for horizontal padding in tables */
+ gutter: {
+ border: 0,
+ margin: 0,
+ padding: 0,
+ fontSize: '1px',
+ lineHeight: '1px',
+ width: `${spacing.gutter}px`,
+ },
+
+ /** Info row (e.g., Platform, Device location, Time) */
+ infoRow: {
+ fontSize: typography.fontSize.body,
+ lineHeight: typography.lineHeight.body,
+ color: colors.textSecondary,
+ fontFamily: typography.fontFamily,
+ margin: '8px 0',
+ },
+}
diff --git a/apps/sim/components/emails/_styles/index.ts b/apps/sim/components/emails/_styles/index.ts
new file mode 100644
index 0000000000..dd1d961d5e
--- /dev/null
+++ b/apps/sim/components/emails/_styles/index.ts
@@ -0,0 +1 @@
+export { baseStyles, colors, spacing, typography } from './base'
diff --git a/apps/sim/components/emails/auth/index.ts b/apps/sim/components/emails/auth/index.ts
new file mode 100644
index 0000000000..b1d04f1dd0
--- /dev/null
+++ b/apps/sim/components/emails/auth/index.ts
@@ -0,0 +1,3 @@
+export { OTPVerificationEmail } from './otp-verification-email'
+export { ResetPasswordEmail } from './reset-password-email'
+export { WelcomeEmail } from './welcome-email'
diff --git a/apps/sim/components/emails/auth/otp-verification-email.tsx b/apps/sim/components/emails/auth/otp-verification-email.tsx
new file mode 100644
index 0000000000..41791adfb6
--- /dev/null
+++ b/apps/sim/components/emails/auth/otp-verification-email.tsx
@@ -0,0 +1,57 @@
+import { Section, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
+import { getBrandConfig } from '@/lib/branding/branding'
+
+interface OTPVerificationEmailProps {
+ otp: string
+ email?: string
+ type?: 'sign-in' | 'email-verification' | 'forget-password' | 'chat-access'
+ chatTitle?: string
+}
+
+const getSubjectByType = (type: string, brandName: string, chatTitle?: string) => {
+ switch (type) {
+ case 'sign-in':
+ return `Sign in to ${brandName}`
+ case 'email-verification':
+ return `Verify your email for ${brandName}`
+ case 'forget-password':
+ return `Reset your ${brandName} password`
+ case 'chat-access':
+ return `Verification code for ${chatTitle || 'Chat'}`
+ default:
+ return `Verification code for ${brandName}`
+ }
+}
+
+export function OTPVerificationEmail({
+ otp,
+ email = '',
+ type = 'email-verification',
+ chatTitle,
+}: OTPVerificationEmailProps) {
+ const brand = getBrandConfig()
+
+ return (
+
+ Your verification code:
+
+
+ {otp}
+
+
+ This code will expire in 15 minutes.
+
+ {/* Divider */}
+
+
+
+ Do not share this code with anyone. If you didn't request this code, you can safely ignore
+ this email.
+
+
+ )
+}
+
+export default OTPVerificationEmail
diff --git a/apps/sim/components/emails/auth/reset-password-email.tsx b/apps/sim/components/emails/auth/reset-password-email.tsx
new file mode 100644
index 0000000000..68f95c2ae9
--- /dev/null
+++ b/apps/sim/components/emails/auth/reset-password-email.tsx
@@ -0,0 +1,36 @@
+import { Link, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
+import { getBrandConfig } from '@/lib/branding/branding'
+
+interface ResetPasswordEmailProps {
+ username?: string
+ resetLink?: string
+}
+
+export function ResetPasswordEmail({ username = '', resetLink = '' }: ResetPasswordEmailProps) {
+ const brand = getBrandConfig()
+
+ return (
+
+ Hello {username},
+
+ A password reset was requested for your {brand.name} account. Click below to set a new
+ password.
+
+
+
+ Reset Password
+
+
+ {/* Divider */}
+
+
+
+ If you didn't request this, you can ignore this email. Link expires in 24 hours.
+
+
+ )
+}
+
+export default ResetPasswordEmail
diff --git a/apps/sim/components/emails/auth/welcome-email.tsx b/apps/sim/components/emails/auth/welcome-email.tsx
new file mode 100644
index 0000000000..bbd196b08d
--- /dev/null
+++ b/apps/sim/components/emails/auth/welcome-email.tsx
@@ -0,0 +1,45 @@
+import { Link, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
+import { getBrandConfig } from '@/lib/branding/branding'
+import { getBaseUrl } from '@/lib/core/utils/urls'
+
+interface WelcomeEmailProps {
+ userName?: string
+}
+
+export function WelcomeEmail({ userName }: WelcomeEmailProps) {
+ const brand = getBrandConfig()
+ const baseUrl = getBaseUrl()
+
+ return (
+
+
+ {userName ? `Hey ${userName},` : 'Hey,'}
+
+
+ Welcome to {brand.name}! Your account is ready. Start building, testing, and deploying AI
+ workflows in minutes.
+
+
+
+ Get Started
+
+
+
+ If you have any questions or feedback, just reply to this email. I read every message!
+
+
+ - Emir, co-founder of {brand.name}
+
+ {/* Divider */}
+
+
+
+ You're on the free plan with $10 in credits to get started.
+
+
+ )
+}
+
+export default WelcomeEmail
diff --git a/apps/sim/components/emails/base-styles.ts b/apps/sim/components/emails/base-styles.ts
deleted file mode 100644
index 633c762eee..0000000000
--- a/apps/sim/components/emails/base-styles.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-// Base styles for all email templates
-
-export const baseStyles = {
- fontFamily: 'HelveticaNeue, Helvetica, Arial, sans-serif',
- main: {
- backgroundColor: '#f5f5f7',
- fontFamily: 'HelveticaNeue, Helvetica, Arial, sans-serif',
- },
- container: {
- maxWidth: '580px',
- margin: '30px auto',
- backgroundColor: '#ffffff',
- borderRadius: '5px',
- overflow: 'hidden',
- },
- header: {
- padding: '30px 0',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: '#ffffff',
- },
- content: {
- padding: '5px 30px 20px 30px',
- },
- paragraph: {
- fontSize: '16px',
- lineHeight: '1.5',
- color: '#333333',
- margin: '16px 0',
- },
- button: {
- display: 'inline-block',
- backgroundColor: '#6F3DFA',
- color: '#ffffff',
- fontWeight: 'bold',
- fontSize: '16px',
- padding: '12px 30px',
- borderRadius: '5px',
- textDecoration: 'none',
- textAlign: 'center' as const,
- margin: '20px 0',
- },
- link: {
- color: '#6F3DFA',
- textDecoration: 'underline',
- },
- footer: {
- maxWidth: '580px',
- margin: '0 auto',
- padding: '20px 0',
- textAlign: 'center' as const,
- },
- footerText: {
- fontSize: '12px',
- color: '#666666',
- margin: '0',
- },
- codeContainer: {
- margin: '20px 0',
- padding: '16px',
- backgroundColor: '#f8f9fa',
- borderRadius: '5px',
- border: '1px solid #eee',
- textAlign: 'center' as const,
- },
- code: {
- fontSize: '28px',
- fontWeight: 'bold',
- letterSpacing: '4px',
- color: '#333333',
- },
- sectionsBorders: {
- width: '100%',
- display: 'flex',
- },
- sectionBorder: {
- borderBottom: '1px solid #eeeeee',
- width: '249px',
- },
- sectionCenter: {
- borderBottom: '1px solid #6F3DFA',
- width: '102px',
- },
-}
diff --git a/apps/sim/components/emails/batch-invitation-email.tsx b/apps/sim/components/emails/batch-invitation-email.tsx
deleted file mode 100644
index fa67ae096c..0000000000
--- a/apps/sim/components/emails/batch-invitation-email.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
-import { getBrandConfig } from '@/lib/branding/branding'
-import { getBaseUrl } from '@/lib/core/utils/urls'
-
-interface WorkspaceInvitation {
- workspaceId: string
- workspaceName: string
- permission: 'admin' | 'write' | 'read'
-}
-
-interface BatchInvitationEmailProps {
- inviterName: string
- organizationName: string
- organizationRole: 'admin' | 'member'
- workspaceInvitations: WorkspaceInvitation[]
- acceptUrl: string
-}
-
-const getPermissionLabel = (permission: string) => {
- switch (permission) {
- case 'admin':
- return 'Admin (full access)'
- case 'write':
- return 'Editor (can edit workflows)'
- case 'read':
- return 'Viewer (read-only access)'
- default:
- return permission
- }
-}
-
-const getRoleLabel = (role: string) => {
- switch (role) {
- case 'admin':
- return 'Admin'
- case 'member':
- return 'Member'
- default:
- return role
- }
-}
-
-export const BatchInvitationEmail = ({
- inviterName = 'Someone',
- organizationName = 'the team',
- organizationRole = 'member',
- workspaceInvitations = [],
- acceptUrl,
-}: BatchInvitationEmailProps) => {
- const brand = getBrandConfig()
- const baseUrl = getBaseUrl()
- const hasWorkspaces = workspaceInvitations.length > 0
-
- return (
-
-
-
-
- You've been invited to join {organizationName}
- {hasWorkspaces ? ` and ${workspaceInvitations.length} workspace(s)` : ''}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hello,
-
- {inviterName} has invited you to join{' '}
- {organizationName} on Sim.
-
-
- {/* Team Role Information */}
-
- Team Role: {getRoleLabel(organizationRole)}
-
-
- {organizationRole === 'admin'
- ? "As a Team Admin, you'll be able to manage team members, billing, and workspace access."
- : "As a Team Member, you'll have access to shared team billing and can be invited to workspaces."}
-
-
- {/* Workspace Invitations */}
- {hasWorkspaces && (
- <>
-
-
- Workspace Access ({workspaceInvitations.length} workspace
- {workspaceInvitations.length !== 1 ? 's' : ''}):
-
-
- {workspaceInvitations.map((ws) => (
-
- • {ws.workspaceName} - {getPermissionLabel(ws.permission)}
-
- ))}
- >
- )}
-
-
- Accept Invitation
-
-
-
- By accepting this invitation, you'll join {organizationName}
- {hasWorkspaces
- ? ` and gain access to ${workspaceInvitations.length} workspace(s)`
- : ''}
- .
-
-
-
- This invitation will expire in 7 days. If you didn't expect this invitation, you can
- safely ignore this email.
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
-
-
-
-
- )
-}
-
-export default BatchInvitationEmail
diff --git a/apps/sim/components/emails/billing/credit-purchase-email.tsx b/apps/sim/components/emails/billing/credit-purchase-email.tsx
index 8668ef9f44..b2c62a0a0e 100644
--- a/apps/sim/components/emails/billing/credit-purchase-email.tsx
+++ b/apps/sim/components/emails/billing/credit-purchase-email.tsx
@@ -1,19 +1,6 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Hr,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Section, Text } from '@react-email/components'
+import { baseStyles, colors } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -36,89 +23,74 @@ export function CreditPurchaseEmail({
const previewText = `${brand.name}: $${amount.toFixed(2)} in credits added to your account`
return (
-
-
-
-
-
-
-
-
-
-
+
+
+ {userName ? `Hi ${userName},` : 'Hi,'}
+
+
+ Your credit purchase of ${amount.toFixed(2)} has been confirmed.
+
-
-
-
-
-
-
-
+
+
+ Amount Added
+
+
+ ${amount.toFixed(2)}
+
+
+ New Balance
+
+
+ ${newBalance.toFixed(2)}
+
+
-
-
- {userName ? `Hi ${userName},` : 'Hi,'}
-
-
- Your credit purchase of ${amount.toFixed(2)} has been confirmed.
-
+
+ Credits are applied automatically to your workflow executions.
+
-
- Amount Added
-
- ${amount.toFixed(2)}
-
- New Balance
-
- ${newBalance.toFixed(2)}
-
-
+
+ View Dashboard
+
-
- These credits will be applied automatically to your workflow executions. Credits are
- consumed before any overage charges apply.
-
+ {/* Divider */}
+
-
- View Dashboard
-
-
-
-
-
- You can view your credit balance and purchase history in Settings → Subscription.
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
- Purchased on {purchaseDate.toLocaleDateString()}
-
-
-
-
-
-
+
+ Purchased on {purchaseDate.toLocaleDateString()}. View balance in Settings → Subscription.
+
+
)
}
diff --git a/apps/sim/components/emails/billing/enterprise-subscription-email.tsx b/apps/sim/components/emails/billing/enterprise-subscription-email.tsx
index f65930ad20..28afdcb72c 100644
--- a/apps/sim/components/emails/billing/enterprise-subscription-email.tsx
+++ b/apps/sim/components/emails/billing/enterprise-subscription-email.tsx
@@ -1,120 +1,50 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { format } from 'date-fns'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
interface EnterpriseSubscriptionEmailProps {
userName?: string
- userEmail?: string
loginLink?: string
- createdDate?: Date
}
-export const EnterpriseSubscriptionEmail = ({
+export function EnterpriseSubscriptionEmail({
userName = 'Valued User',
- userEmail = '',
loginLink,
- createdDate = new Date(),
-}: EnterpriseSubscriptionEmailProps) => {
+}: EnterpriseSubscriptionEmailProps) {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
const effectiveLoginLink = loginLink || `${baseUrl}/login`
return (
-
-
- Your Enterprise Plan is now active on Sim
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hello {userName},
-
- Great news! Your Enterprise Plan has been activated on Sim. You now
- have access to advanced features and increased capacity for your workflows.
-
-
-
- Your account has been set up with full access to your organization. Click below to log
- in and start exploring your new Enterprise features:
-
-
-
- Access Your Enterprise Account
-
-
-
- What's next?
-
-
- • Invite team members to your organization
- • Begin building your workflows
-
-
-
- If you have any questions or need assistance getting started, our support team is here
- to help.
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
- This email was sent on {format(createdDate, 'MMMM do, yyyy')} to {userEmail}
- regarding your Enterprise plan activation on Sim.
-
-
-
-
-
-
-
+
+ Hello {userName},
+
+ Your Enterprise Plan is now active. You have full access to advanced
+ features and increased capacity for your workflows.
+
+
+
+ Open {brand.name}
+
+
+
+ Next steps:
+ • Invite team members to your organization
+ • Start building your workflows
+
+
+ {/* Divider */}
+
+
+
+ Questions? Reply to this email or contact us at{' '}
+
+ {brand.supportEmail}
+
+
+
)
}
diff --git a/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx b/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx
index 857a3e9a9a..c18d7bc17c 100644
--- a/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx
+++ b/apps/sim/components/emails/billing/free-tier-upgrade-email.tsx
@@ -1,21 +1,7 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Hr,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Section, Text } from '@react-email/components'
+import { baseStyles, colors, typography } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
-import { getBaseUrl } from '@/lib/core/utils/urls'
interface FreeTierUpgradeEmailProps {
userName?: string
@@ -23,119 +9,105 @@ interface FreeTierUpgradeEmailProps {
currentUsage: number
limit: number
upgradeLink: string
- updatedDate?: Date
}
+const proFeatures = [
+ { label: '$20/month', desc: 'in credits included' },
+ { label: '25 runs/min', desc: 'sync executions' },
+ { label: '200 runs/min', desc: 'async executions' },
+ { label: '50GB storage', desc: 'for files & assets' },
+ { label: 'Unlimited', desc: 'workspaces & invites' },
+]
+
export function FreeTierUpgradeEmail({
userName,
percentUsed,
currentUsage,
limit,
upgradeLink,
- updatedDate = new Date(),
}: FreeTierUpgradeEmailProps) {
const brand = getBrandConfig()
- const baseUrl = getBaseUrl()
const previewText = `${brand.name}: You've used ${percentUsed}% of your free credits`
return (
-
-
-
-
-
-
-
+
+ {userName ? `Hi ${userName},` : 'Hi,'}
+
+
+
+ You've used ${currentUsage.toFixed(2)} of your{' '}
+ ${limit.toFixed(2)} free credits ({percentUsed}%). Upgrade to Pro to keep
+ building without interruption.
+
+
+ {/* Pro Features */}
+
+
+ Pro includes
+
+
+
+ {proFeatures.map((feature, i) => (
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {userName ? `Hi ${userName},` : 'Hi,'}
-
-
-
- You've used ${currentUsage.toFixed(2)} of your{' '}
- ${limit.toFixed(2)} free credits ({percentUsed}%).
-
-
-
- To ensure uninterrupted service and unlock the full power of {brand.name}, upgrade to
- Pro today.
-
-
-
-
- What you get with Pro:
-
-
- • $20/month in credits – 2x your free tier
- • Priority support – Get help when you need it
- • Advanced features – Access to premium blocks and
- integrations
- • No interruptions – Never worry about running out of credits
-
-
-
-
-
- Upgrade now to keep building without limits.
-
-
- Upgrade to Pro
-
-
-
- Questions? We're here to help.
-
-
- Best regards,
-
- The {brand.name} Team
-
-
-
- Sent on {updatedDate.toLocaleDateString()} • This is a one-time notification at 90%.
-
-
-
-
-
-
-
+ >
+ {feature.label}
+
+
+ {feature.desc}
+
+
+ ))}
+
+
+
+
+
+ Upgrade to Pro
+
+
+ {/* Divider */}
+
+
+
+ One-time notification at 90% usage.
+
+
)
}
diff --git a/apps/sim/components/emails/billing/index.ts b/apps/sim/components/emails/billing/index.ts
new file mode 100644
index 0000000000..81f25ebfbd
--- /dev/null
+++ b/apps/sim/components/emails/billing/index.ts
@@ -0,0 +1,6 @@
+export { CreditPurchaseEmail } from './credit-purchase-email'
+export { EnterpriseSubscriptionEmail } from './enterprise-subscription-email'
+export { FreeTierUpgradeEmail } from './free-tier-upgrade-email'
+export { PaymentFailedEmail } from './payment-failed-email'
+export { PlanWelcomeEmail } from './plan-welcome-email'
+export { UsageThresholdEmail } from './usage-threshold-email'
diff --git a/apps/sim/components/emails/billing/payment-failed-email.tsx b/apps/sim/components/emails/billing/payment-failed-email.tsx
index 17d9087a20..eb982fe391 100644
--- a/apps/sim/components/emails/billing/payment-failed-email.tsx
+++ b/apps/sim/components/emails/billing/payment-failed-email.tsx
@@ -1,21 +1,7 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Hr,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Section, Text } from '@react-email/components'
+import { baseStyles, colors } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
-import { getBaseUrl } from '@/lib/core/utils/urls'
interface PaymentFailedEmailProps {
userName?: string
@@ -35,132 +21,88 @@ export function PaymentFailedEmail({
sentDate = new Date(),
}: PaymentFailedEmailProps) {
const brand = getBrandConfig()
- const baseUrl = getBaseUrl()
const previewText = `${brand.name}: Payment Failed - Action Required`
return (
-
-
- {previewText}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {userName ? `Hi ${userName},` : 'Hi,'}
-
-
-
- We were unable to process your payment.
-
-
-
- Your {brand.name} account has been temporarily blocked to prevent service
- interruptions and unexpected charges. To restore access immediately, please update
- your payment method.
-
-
-
-
-
-
- Payment Details
-
-
- Amount due: ${amountDue.toFixed(2)}
-
- {lastFourDigits && (
-
- Payment method: •••• {lastFourDigits}
-
- )}
- {failureReason && (
-
- Reason: {failureReason}
-
- )}
-
-
-
-
-
- Update Payment Method
-
-
-
-
-
- What happens next?
-
-
-
- • Your workflows and automations are currently paused
- • Update your payment method to restore service immediately
- • Stripe will automatically retry the charge once payment is updated
-
-
-
-
-
- Need help?
-
-
-
- Common reasons for payment failures include expired cards, insufficient funds, or
- incorrect billing information. If you continue to experience issues, please{' '}
-
- contact our support team
-
- .
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
- Sent on {sentDate.toLocaleDateString()} • This is a critical transactional
- notification.
-
-
-
-
-
-
-
+
+
+ {userName ? `Hi ${userName},` : 'Hi,'}
+
+
+
+ We were unable to process your payment.
+
+
+
+ Your {brand.name} account has been temporarily blocked to prevent service interruptions and
+ unexpected charges. To restore access immediately, please update your payment method.
+
+
+
+
+ Payment Details
+
+
+ Amount due: ${amountDue.toFixed(2)}
+
+ {lastFourDigits && (
+
+ Payment method: •••• {lastFourDigits}
+
+ )}
+ {failureReason && (
+ Reason: {failureReason}
+ )}
+
+
+
+ Update Payment Method
+
+
+ {/* Divider */}
+
+
+ What happens next?
+
+
+ • Your workflows and automations are currently paused
+ • Update your payment method to restore service immediately
+ • Stripe will automatically retry the charge once payment is updated
+
+
+ {/* Divider */}
+
+
+
+ Common issues: expired card, insufficient funds, or incorrect billing info. Need help?{' '}
+
+ {brand.supportEmail}
+
+
+
)
}
diff --git a/apps/sim/components/emails/billing/plan-welcome-email.tsx b/apps/sim/components/emails/billing/plan-welcome-email.tsx
index fbac398b81..253d4a39fd 100644
--- a/apps/sim/components/emails/billing/plan-welcome-email.tsx
+++ b/apps/sim/components/emails/billing/plan-welcome-email.tsx
@@ -1,19 +1,6 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Hr,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -21,15 +8,9 @@ interface PlanWelcomeEmailProps {
planName: 'Pro' | 'Team'
userName?: string
loginLink?: string
- createdDate?: Date
}
-export function PlanWelcomeEmail({
- planName,
- userName,
- loginLink,
- createdDate = new Date(),
-}: PlanWelcomeEmailProps) {
+export function PlanWelcomeEmail({ planName, userName, loginLink }: PlanWelcomeEmailProps) {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
const cta = loginLink || `${baseUrl}/login`
@@ -37,76 +18,34 @@ export function PlanWelcomeEmail({
const previewText = `${brand.name}: Your ${planName} plan is active`
return (
-
-
- {previewText}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {userName ? `Hi ${userName},` : 'Hi,'}
-
-
- Welcome to the {planName} plan on {brand.name}. You're all set to
- build, test, and scale your agentic workflows.
-
-
-
- Open {brand.name}
-
-
-
- Want to discuss your plan or get personalized help getting started?{' '}
-
- Schedule a 15-minute call
- {' '}
- with our team.
-
-
-
-
-
- Need to invite teammates, adjust usage limits, or manage billing? You can do that from
- Settings → Subscription.
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
- Sent on {createdDate.toLocaleDateString()}
-
-
-
-
-
-
+
+
+ {userName ? `Hi ${userName},` : 'Hi,'}
+
+
+ Welcome to {planName}! You're all set to build, test, and scale your
+ workflows.
+
+
+
+ Open {brand.name}
+
+
+
+ Want help getting started?{' '}
+
+ Schedule a call
+ {' '}
+ with our team.
+
+
+ {/* Divider */}
+
+
+
+ Manage your subscription in Settings → Subscription.
+
+
)
}
diff --git a/apps/sim/components/emails/billing/usage-threshold-email.tsx b/apps/sim/components/emails/billing/usage-threshold-email.tsx
index 1885e2e60a..fdcb01c326 100644
--- a/apps/sim/components/emails/billing/usage-threshold-email.tsx
+++ b/apps/sim/components/emails/billing/usage-threshold-email.tsx
@@ -1,21 +1,7 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Hr,
- Html,
- Img,
- Link,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { Link, Section, Text } from '@react-email/components'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
-import { getBaseUrl } from '@/lib/core/utils/urls'
interface UsageThresholdEmailProps {
userName?: string
@@ -24,7 +10,6 @@ interface UsageThresholdEmailProps {
currentUsage: number
limit: number
ctaLink: string
- updatedDate?: Date
}
export function UsageThresholdEmail({
@@ -34,89 +19,46 @@ export function UsageThresholdEmail({
currentUsage,
limit,
ctaLink,
- updatedDate = new Date(),
}: UsageThresholdEmailProps) {
const brand = getBrandConfig()
- const baseUrl = getBaseUrl()
const previewText = `${brand.name}: You're at ${percentUsed}% of your ${planName} monthly budget`
return (
-
-
- {previewText}
-
-
-
-
-
-
-
-
-
+
+
+ {userName ? `Hi ${userName},` : 'Hi,'}
+
-
-
-
-
-
-
-
+
+ You're approaching your monthly budget on the {planName} plan.
+
-
-
- {userName ? `Hi ${userName},` : 'Hi,'}
-
+
+ Usage
+
+ ${currentUsage.toFixed(2)} of ${limit.toFixed(2)} used ({percentUsed}%)
+
+
-
- You're approaching your monthly budget on the {planName} plan.
-
+ {/* Divider */}
+
-
-
-
-
- Usage
-
-
- ${currentUsage.toFixed(2)} of ${limit.toFixed(2)} used ({percentUsed}%)
-
-
-
-
+
+ To avoid interruptions, consider increasing your monthly limit.
+
-
+
+ Review Limits
+
-
- To avoid interruptions, consider increasing your monthly limit.
-
+ {/* Divider */}
+
-
- Review limits
-
-
-
- Best regards,
-
- The Sim Team
-
-
-
- Sent on {updatedDate.toLocaleDateString()} • This is a one-time notification at 80%.
-
-
-
-
-
-
-
+
+ One-time notification at 80% usage.
+
+
)
}
diff --git a/apps/sim/components/emails/careers/careers-confirmation-email.tsx b/apps/sim/components/emails/careers/careers-confirmation-email.tsx
index 6c15c1c09a..beb07e1daf 100644
--- a/apps/sim/components/emails/careers/careers-confirmation-email.tsx
+++ b/apps/sim/components/emails/careers/careers-confirmation-email.tsx
@@ -1,18 +1,7 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Html,
- Img,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
+import { Text } from '@react-email/components'
import { format } from 'date-fns'
-import { baseStyles } from '@/components/emails/base-styles'
-import EmailFooter from '@/components/emails/footer'
+import { baseStyles } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -22,96 +11,46 @@ interface CareersConfirmationEmailProps {
submittedDate?: Date
}
-export const CareersConfirmationEmail = ({
+export function CareersConfirmationEmail({
name,
position,
submittedDate = new Date(),
-}: CareersConfirmationEmailProps) => {
+}: CareersConfirmationEmailProps) {
const brand = getBrandConfig()
const baseUrl = getBaseUrl()
return (
-
-
-
- Your application to {brand.name} has been received
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hello {name},
-
- Thank you for your interest in joining the {brand.name} team! We've received your
- application for the {position} position.
-
-
-
- Our team carefully reviews every application and will get back to you within the next
- few weeks. If your qualifications match what we're looking for, we'll reach out to
- schedule an initial conversation.
-
-
-
- In the meantime, feel free to explore our{' '}
-
- documentation
- {' '}
- to learn more about what we're building, or check out our{' '}
-
- blog
- {' '}
- for the latest updates.
-
-
-
- Best regards,
-
- The {brand.name} Team
-
-
-
- This confirmation was sent on {format(submittedDate, 'MMMM do, yyyy')} at{' '}
- {format(submittedDate, 'h:mm a')}.
-
-
-
-
-
-
-
+
+ Hello {name},
+
+ We've received your application for {position}. Our team reviews every
+ application and will reach out if there's a match.
+
+
+
+ In the meantime, explore our{' '}
+
+ docs
+ {' '}
+ or{' '}
+
+ blog
+ {' '}
+ to learn more about what we're building.
+
+
+ {/* Divider */}
+
+
+
+ Submitted on {format(submittedDate, 'MMMM do, yyyy')}.
+
+
)
}
diff --git a/apps/sim/components/emails/careers/careers-submission-email.tsx b/apps/sim/components/emails/careers/careers-submission-email.tsx
index 9beed5d3ad..0d12664beb 100644
--- a/apps/sim/components/emails/careers/careers-submission-email.tsx
+++ b/apps/sim/components/emails/careers/careers-submission-email.tsx
@@ -1,19 +1,7 @@
-import {
- Body,
- Column,
- Container,
- Head,
- Html,
- Img,
- Preview,
- Row,
- Section,
- Text,
-} from '@react-email/components'
+import { Section, Text } from '@react-email/components'
import { format } from 'date-fns'
-import { baseStyles } from '@/components/emails/base-styles'
-import { getBrandConfig } from '@/lib/branding/branding'
-import { getBaseUrl } from '@/lib/core/utils/urls'
+import { baseStyles, colors } from '@/components/emails/_styles'
+import { EmailLayout } from '@/components/emails/components'
interface CareersSubmissionEmailProps {
name: string
@@ -39,7 +27,7 @@ const getExperienceLabel = (experience: string) => {
return labels[experience] || experience
}
-export const CareersSubmissionEmail = ({
+export function CareersSubmissionEmail({
name,
email,
phone,
@@ -50,263 +38,299 @@ export const CareersSubmissionEmail = ({
location,
message,
submittedDate = new Date(),
-}: CareersSubmissionEmailProps) => {
- const brand = getBrandConfig()
- const baseUrl = getBaseUrl()
-
+}: CareersSubmissionEmailProps) {
return (
-
-
- New Career Application from {name}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ New Career Application
+
-
-
- New Career Application
-
+
+ A new career application has been submitted on {format(submittedDate, 'MMMM do, yyyy')} at{' '}
+ {format(submittedDate, 'h:mm a')}.
+
-
- A new career application has been submitted on{' '}
- {format(submittedDate, 'MMMM do, yyyy')} at {format(submittedDate, 'h:mm a')}.
-
+ {/* Applicant Information */}
+
+
+ Applicant Information
+
- {/* Applicant Information */}
-
-
+
+