@fyit/crouton-email
v0.1.0
Published
Email infrastructure for Nuxt applications using Vue Email templates and Resend delivery
Maintainers
Readme
@crouton/email
Email infrastructure layer for Nuxt applications using Vue Email templates and Resend delivery.
Features
- Server-side email utilities - Send emails via Resend with typed interfaces
- Pre-built Vue Email templates - Verification, magic link, password reset, team invite, welcome
- Client-side flow components - Ready-to-use verification and magic link UI flows
- Brand customization - Configure logo, colors, and app name for all templates
- Standalone or integrated - Works alone or enhances
@crouton/auth
Installation
pnpm add @crouton/email vue-email resendQuick Start
1. Add the layer to your Nuxt config
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@crouton/email'],
runtimeConfig: {
email: {
resendApiKey: process.env.RESEND_API_KEY,
from: '[email protected]',
fromName: 'My App',
},
public: {
crouton: {
email: {
brand: {
name: 'My App',
primaryColor: '#0F766E',
url: 'https://example.com',
},
},
},
},
},
})2. Set environment variables
RESEND_API_KEY=re_xxx3. Send emails from server
// server/api/auth/verify.post.ts
export default defineEventHandler(async (event) => {
const { email, code } = await readBody(event)
await sendVerificationEmail({
to: email,
code,
name: 'John',
})
return { success: true }
})4. Use client components
<template>
<EmailVerificationFlow
:email="userEmail"
@verified="handleVerified"
@resend="handleResend"
/>
</template>Server Utilities
Email Service
Low-level email sending with full control:
const emailService = useEmailService()
const result = await emailService.send({
to: '[email protected]',
subject: 'Hello',
html: '<p>Hello World</p>',
})Convenience Senders
Pre-built functions for common email types:
// Verification code email
await sendVerificationEmail({
to: '[email protected]',
code: '123456',
name: 'John',
expiryMinutes: 10,
})
// Magic link login
await sendMagicLink({
to: '[email protected]',
link: 'https://app.com/auth/magic?token=xxx',
name: 'John',
})
// Password reset
await sendPasswordReset({
to: '[email protected]',
link: 'https://app.com/auth/reset?token=xxx',
name: 'John',
})
// Team invitation
await sendTeamInvite({
to: '[email protected]',
link: 'https://app.com/invite/accept?token=xxx',
inviterName: 'Jane',
teamName: 'Acme Inc',
role: 'member',
})
// Welcome email
await sendWelcome({
to: '[email protected]',
name: 'John',
getStartedLink: 'https://app.com/getting-started',
})Client Components
EmailVerificationFlow
Complete verification code input with resend functionality.
<template>
<EmailVerificationFlow
:email="userEmail"
:code-length="6"
:resend-cooldown="60"
:loading="isVerifying"
:error="verificationError"
@verified="handleVerified"
@resend="handleResend"
/>
</template>
<script setup>
const handleVerified = (code) => {
// Submit code to your API
}
const handleResend = () => {
// Request new verification code
}
</script>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| email | string | required | Email being verified |
| codeLength | number | 6 | Expected code length |
| resendCooldown | number | 60 | Cooldown in seconds |
| loading | boolean | false | Loading state |
| error | string | '' | Error message |
Events:
| Event | Payload | Description |
|-------|---------|-------------|
| verified | code: string | Code submitted |
| resend | - | Resend requested |
| error | error: string | Error occurred |
EmailMagicLinkSent
"Check your email" message for magic link flows.
<template>
<EmailMagicLinkSent
:email="userEmail"
:resend-cooldown="60"
@resend="handleResend"
@change-email="handleChangeEmail"
/>
</template>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| email | string | required | Email where link was sent |
| resendCooldown | number | 60 | Cooldown in seconds |
| loading | boolean | false | Loading state |
| error | string | '' | Error message |
Events:
| Event | Payload | Description |
|-------|---------|-------------|
| resend | - | Resend requested |
| change-email | - | Change email requested |
EmailResendButton
Standalone resend button with countdown timer.
<template>
<EmailResendButton
:cooldown="60"
:loading="isResending"
:disabled="false"
@resend="handleResend"
/>
</template>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| cooldown | number | 60 | Cooldown in seconds |
| loading | boolean | false | Loading state |
| disabled | boolean | false | Disabled state |
Events:
| Event | Payload | Description |
|-------|---------|-------------|
| resend | - | Button clicked (after cooldown) |
EmailInput
Email input with real-time validation.
<template>
<EmailInput
v-model="email"
:error="emailError"
placeholder="Enter your email"
@valid="handleValidation"
/>
</template>Props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| modelValue | string | '' | Email value (v-model) |
| error | string | '' | External error message |
| placeholder | string | '[email protected]' | Input placeholder |
| disabled | boolean | false | Disabled state |
| autofocus | boolean | false | Autofocus on mount |
Events:
| Event | Payload | Description |
|-------|---------|-------------|
| update:modelValue | email: string | Email changed |
| valid | isValid: boolean | Validation state |
Email Templates
Pre-built Vue Email templates in server/emails/:
| Template | Purpose | Key Props |
|----------|---------|-----------|
| BaseLayout.vue | Shared layout | brandName, logoUrl, primaryColor |
| Verification.vue | Verification code | code, name, expiryMinutes |
| MagicLink.vue | Magic link login | link, name, expiryMinutes |
| PasswordReset.vue | Password reset | link, name, expiryMinutes |
| TeamInvite.vue | Team invitation | link, inviterName, teamName, role |
| Welcome.vue | Welcome email | name, getStartedLink |
Configuration
Full Configuration
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@crouton/email'],
runtimeConfig: {
// Server-only (private)
email: {
resendApiKey: process.env.RESEND_API_KEY,
from: '[email protected]',
fromName: 'My App',
replyTo: '[email protected]', // optional
},
// Public (available on client)
public: {
crouton: {
email: {
brand: {
name: 'My App',
logoUrl: 'https://example.com/logo.png', // optional
primaryColor: '#0F766E',
url: 'https://example.com',
},
verification: {
codeLength: 6,
codeExpiry: 10, // minutes
resendCooldown: 60, // seconds
},
magicLink: {
expiry: 10, // minutes
resendCooldown: 60, // seconds
},
},
},
},
},
})Environment Variables
# Required
RESEND_API_KEY=re_xxx
# Optional (can be set in nuxt.config.ts instead)
[email protected]
EMAIL_FROM_NAME=My App
[email protected]Integration with @crouton/auth
When used with @crouton/auth, email utilities are automatically available for:
- Email verification on signup
- Magic link authentication
- Password reset emails
- Team invitation emails
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@crouton/auth', '@crouton/email'],
runtimeConfig: {
email: {
resendApiKey: process.env.RESEND_API_KEY,
from: '[email protected]',
fromName: 'My App',
},
},
})Standalone Usage
Works without @crouton/auth. Implement your own endpoints:
// server/api/auth/send-code.post.ts
export default defineEventHandler(async (event) => {
const { email } = await readBody(event)
// Generate your verification code
const code = Math.random().toString().slice(2, 8)
// Save code to your database
await saveVerificationCode(email, code)
// Send email
await sendVerificationEmail({ to: email, code })
return { success: true }
})// server/api/auth/verify-code.post.ts
export default defineEventHandler(async (event) => {
const { email, code } = await readBody(event)
// Verify code from your database
const isValid = await verifyCode(email, code)
if (!isValid) {
throw createError({ statusCode: 400, message: 'Invalid code' })
}
return { success: true }
})Custom Templates
Create your own templates using the base layout:
<!-- server/emails/CustomEmail.vue -->
<script setup lang="ts">
import { EBody, EContainer, EText, EButton, ESection } from 'vue-email'
import BaseLayout from './BaseLayout.vue'
interface Props {
customProp: string
brandName?: string
primaryColor?: string
}
defineProps<Props>()
</script>
<template>
<BaseLayout :brand-name="brandName" :primary-color="primaryColor">
<ESection>
<EText>Your custom content: {{ customProp }}</EText>
</ESection>
</BaseLayout>
</template>Then create a sender function:
// server/utils/custom-senders.ts
import CustomEmail from '../emails/CustomEmail.vue'
import { renderEmailTemplate, getEmailBrandConfig } from './template-renderer'
import { useEmailService } from './email'
export async function sendCustomEmail(options: {
to: string
customProp: string
}) {
const brandConfig = getEmailBrandConfig()
const { html, text } = await renderEmailTemplate(CustomEmail, {
customProp: options.customProp,
...brandConfig,
})
return useEmailService().send({
to: options.to,
subject: 'Custom Email',
html,
text,
})
}TypeScript
All interfaces are exported from types/index.ts:
import type {
SendEmailOptions,
SendEmailResult,
VerificationEmailOptions,
MagicLinkEmailOptions,
PasswordResetEmailOptions,
TeamInviteEmailOptions,
WelcomeEmailOptions,
EmailConfig,
EmailPublicConfig,
} from '@crouton/email/types'Testing
Use Resend's test mode for development:
# Use Resend's test API key (emails won't be delivered)
RESEND_API_KEY=re_test_xxxLicense
MIT
