Batteries

Email System

Grit integrates with Resend for transactional email delivery using raw net/http calls (no SDK dependency). It includes a Mailer service, Go template rendering, and four built-in email templates styled with the Grit dark theme.

Configuration

Email sending requires a Resend API key and a verified sender address. For local development, Mailhog is included in Docker Compose for testing without sending real emails.

.env
# Email Configuration
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
MAIL_FROM=noreply@yourdomain.com

Mailer Service

The Mailer service at internal/mail/mailer.go provides two methods for sending emails: Send() for template-based emails and SendRaw() for raw HTML emails.

internal/mail/mailer.go
// Mailer sends emails via the Resend API.
type Mailer struct {
    apiKey string
    from   string
    client *http.Client
}

// New creates a new Mailer instance.
func New(apiKey, from string) *Mailer

// SendOptions configures an email to send.
type SendOptions struct {
    To       string
    Subject  string
    Template string                 // Template name (e.g., "welcome")
    Data     map[string]interface{} // Template variables
}

// Send renders a template and sends the email via Resend.
func (m *Mailer) Send(ctx context.Context, opts SendOptions) error

// SendRaw sends an email with raw HTML content (no template).
func (m *Mailer) SendRaw(ctx context.Context, to, subject, htmlBody string) error

Usage Examples

send-welcome-email.go
// Send a welcome email using a template
err := mailer.Send(ctx, mail.SendOptions{
    To:       "user@example.com",
    Subject:  "Welcome to MyApp!",
    Template: "welcome",
    Data: map[string]interface{}{
        "AppName":      "MyApp",
        "Name":         "John",
        "DashboardURL": "https://myapp.com/dashboard",
        "Year":         time.Now().Year(),
    },
})

// Send a password reset email
err = mailer.Send(ctx, mail.SendOptions{
    To:       "user@example.com",
    Subject:  "Reset Your Password",
    Template: "password-reset",
    Data: map[string]interface{}{
        "AppName":  "MyApp",
        "ResetURL": "https://myapp.com/reset?token=abc123",
        "Year":     time.Now().Year(),
    },
})

// Send a raw HTML email
err = mailer.SendRaw(ctx, "user@example.com", "Custom Email", "<h1>Hello!</h1>")

Template Rendering

Email templates use Go's html/template package. Templates are stored as Go constants in internal/mail/templates.go and compiled at render time. Variables use the standard Go template syntax: {{.VariableName}}.

internal/mail/templates.go (registry)
// EmailTemplates contains all available email templates.
var EmailTemplates = map[string]string{
    "welcome":            welcomeTemplate,
    "password-reset":     passwordResetTemplate,
    "email-verification": emailVerificationTemplate,
    "notification":       notificationTemplate,
}

Built-in Templates

Grit ships four pre-built email templates, all styled with the dark theme (background: #0a0a0f, accent: #6c5ce7). Each template is a complete HTML document with inline CSS for maximum email client compatibility.

TemplateVariablesUse Case
welcomeAppName, Name, DashboardURL, YearAfter user registration
password-resetAppName, ResetURL, YearPassword reset request
email-verificationAppName, VerifyURL, YearEmail address verification
notificationAppName, Title, Message, ActionURL, ActionText, YearGeneric notifications

Template Structure

Each template follows a consistent structure: dark background, card container with logo, heading, body text, optional CTA button, and footer. Here is the welcome template:

welcome template (simplified)
<body style="background-color: #0a0a0f; color: #e8e8f0;">
  <div class="container">
    <div class="card">
      <div class="logo">{{.AppName}}</div>
      <h1>Welcome, {{.Name}}!</h1>
      <p>Thanks for signing up. Your account is ready to use.</p>
      <p>Get started by exploring the dashboard:</p>
      <p style="text-align: center;">
        <a href="{{.DashboardURL}}" class="btn">Go to Dashboard</a>
      </p>
    </div>
    <div class="footer">
      <p>&copy; {{.Year}} {{.AppName}}. All rights reserved.</p>
    </div>
  </div>
</body>

Adding Custom Templates

To add a new email template, define it as a Go constant in templates.go and register it in the EmailTemplates map.

internal/mail/templates.go
// Add to the EmailTemplates map:
var EmailTemplates = map[string]string{
    "welcome":            welcomeTemplate,
    "password-reset":     passwordResetTemplate,
    "email-verification": emailVerificationTemplate,
    "notification":       notificationTemplate,
    "invoice":            invoiceTemplate,  // Your custom template
}

// Define the template:
const invoiceTemplate = `<!DOCTYPE html>
<html>
<head>...</head>
<body>
  <div class="container">
    <div class="card">
      <div class="logo">{{.AppName}}</div>
      <h1>Invoice #{{.InvoiceNumber}}</h1>
      <p>Amount: {{.Amount}}</p>
      <p>Due: {{.DueDate}}</p>
      <a href="{{.PaymentURL}}" class="btn">Pay Now</a>
    </div>
  </div>
</body>
</html>`

// Send it:
mailer.Send(ctx, mail.SendOptions{
    To:       "customer@example.com",
    Subject:  "Invoice #INV-001",
    Template: "invoice",
    Data: map[string]interface{}{
        "AppName":       "MyApp",
        "InvoiceNumber": "INV-001",
        "Amount":        "99.00",
        "DueDate":       "Feb 28, 2026",
        "PaymentURL":    "https://myapp.com/pay/inv-001",
    },
})

Background Email Sending

For better API response times, emails should be sent via background jobs rather than inline. Grit's job queue client provides a EnqueueSendEmail() method that sends the email asynchronously.

async-email.go
// Instead of sending synchronously:
// mailer.Send(ctx, opts)

// Enqueue a background job (returns immediately):
err := jobsClient.EnqueueSendEmail(
    "user@example.com",
    "Welcome to MyApp!",
    "welcome",
    map[string]interface{}{
        "AppName":      "MyApp",
        "Name":         "John",
        "DashboardURL": "https://myapp.com/dashboard",
        "Year":         2026,
    },
)
// The asynq worker picks it up and calls mailer.Send()

Admin email preview: The admin panel includes an email template preview page where you can view how each template renders with sample data before sending. See the Background Jobs page for the full job queue documentation.