Email Workflow

Last updated on Jun 12, 2026

Understanding how email notifications work in CHRIS.

Overview

CHRIS sends automated email notifications for key events:

  • Leave request submissions
  • Leave request approvals/rejections
  • Welcome emails for new employees
  • Password reset requests

Architecture

┌─────────────┐     ┌──────────────────┐     ┌─────────────┐
│   Frontend  │────▶│  Edge Function   │────▶│ SMTP Server │
│             │     │  (Deno Runtime)  │     │             │
└─────────────┘     └────────┬─────────┘     └──────┬──────┘
                             │                      │
                             ▼                      ▼
                    ┌─────────────────┐     ┌─────────────┐
                    │ company_settings│     │  Recipient  │
                    │  (SMTP config)  │     │   Inbox     │
                    └─────────────────┘     └─────────────┘

Email Types

Leave Request Submitted

Trigger: Employee submits a new leave request

Recipients:

  • Team leader (if assigned)
  • HR managers

Content:

  • Employee name and email
  • Leave type (Annual, Sick, etc.)
  • Date range
  • Working days requested
  • Reason (if provided)

Template Variables:

{{employee_name}}
{{employee_email}}
{{leave_type}}
{{start_date}}
{{end_date}}
{{working_days}}
{{reason}}

Leave Request Status Update

Trigger: Request is approved or rejected

Recipients:

  • The employee who submitted the request

Content:

  • Decision (Approved/Rejected)
  • Approver's name
  • Comments (if any)
  • Original request details

Template Variables:

{{employee_name}}
{{status}}
{{approver_name}}
{{approver_comment}}
{{leave_type}}
{{start_date}}
{{end_date}}
{{working_days}}

Welcome Email

Trigger: New employee created

Recipients:

  • The new employee

Content:

  • Welcome message
  • Login instructions
  • Getting started guide

Template Variables:

{{employee_name}}
{{email}}
{{position}}
{{start_date}}

Password Reset

Trigger: Admin initiates password reset

Recipients:

  • The employee

Content:

  • Reset link (time-limited)
  • Instructions

SMTP Configuration

Settings Location

SMTP settings are stored in company_settings:

Field Description
smtp_host Server address (e.g., smtp.gmail.com)
smtp_port Port number (usually 587)
smtp_username Authentication email
smtp_password SMTP password (encrypted)
smtp_from_email Sender email address
smtp_from_name Sender display name
smtp_use_tls Enable TLS encryption
smtp_enabled Master enable/disable

Provider Examples

=== "Gmail"

```
Host: smtp.gmail.com
Port: 587
TLS: Enabled
Username: your-email@gmail.com
Password: App-specific password
```

=== "Outlook"

```
Host: smtp-mail.outlook.com
Port: 587
TLS: Enabled
Username: your-email@outlook.com
Password: App-specific password
```

=== "Custom SMTP"

Consult your provider's documentation for correct settings.

Email Flow

Sending an Email

// src/utils/emailNotifications.ts

export async function sendNotificationEmail({
  to,
  subject,
  html,
  text
}: EmailParams) {
  const { data, error } = await supabase.functions.invoke(
    'send-notification-email',
    {
      body: { to, subject, html, text }
    }
  );

  if (error) {
    console.error('Email failed:', error);
    // Don't throw - email failure shouldn't block operations
  }

  return { success: !error };
}

Edge Function Processing

// supabase/functions/send-notification-email/index.ts

Deno.serve(async (req) => {
  // 1. Verify JWT
  const token = req.headers.get('Authorization')?.replace('Bearer ', '');
  const { data: { user } } = await supabase.auth.getUser(token);
  if (!user) return new Response('Unauthorized', { status: 401 });

  // 2. Get request body
  const { to, subject, html, text } = await req.json();

  // 3. Get SMTP settings
  const { data: settings } = await supabase
    .from('company_settings')
    .select('*')
    .single();

  if (!settings.smtp_enabled) {
    return new Response(JSON.stringify({
      success: false,
      error: 'SMTP not enabled'
    }));
  }

  // 4. Check for rerouting
  let recipients = to;
  if (settings.email_reroute_enabled) {
    recipients = [settings.email_reroute_addresses];
  }

  // 5. Send via SMTP
  await sendViaSMTP(settings, recipients, subject, html, text);

  return new Response(JSON.stringify({ success: true }));
});

Email Templates

Template Storage

Templates stored in email_templates table:

CREATE TABLE email_templates (
  id UUID PRIMARY KEY,
  template_key TEXT UNIQUE NOT NULL,
  name TEXT NOT NULL,
  subject TEXT NOT NULL,
  html_content TEXT NOT NULL,
  description TEXT,
  variables JSONB,
  is_active BOOLEAN DEFAULT true
);

Template Example

<!-- leave_request_approved -->
<div style="font-family: Arial, sans-serif; max-width: 600px;">
  <h2>Leave Request Approved</h2>

  <p>Dear {{employee_name}},</p>

  <p>Your leave request has been <strong>approved</strong>.</p>

  <table style="border-collapse: collapse; width: 100%;">
    <tr>
      <td style="padding: 8px; border: 1px solid #ddd;">Leave Type</td>
      <td style="padding: 8px; border: 1px solid #ddd;">{{leave_type}}</td>
    </tr>
    <tr>
      <td style="padding: 8px; border: 1px solid #ddd;">Dates</td>
      <td style="padding: 8px; border: 1px solid #ddd;">
        {{start_date}} - {{end_date}}
      </td>
    </tr>
    <tr>
      <td style="padding: 8px; border: 1px solid #ddd;">Working Days</td>
      <td style="padding: 8px; border: 1px solid #ddd;">{{working_days}}</td>
    </tr>
  </table>

  <p>Approved by: {{approver_name}}</p>

  {{#if approver_comment}}
  <p>Comment: {{approver_comment}}</p>
  {{/if}}

  <p>Best regards,<br>HR Team</p>
</div>

Variable Substitution

function renderTemplate(
  template: string,
  variables: Record<string, string>
): string {
  return template.replace(
    /\{\{(\w+)\}\}/g,
    (match, key) => variables[key] || match
  );
}

Email Rerouting

Purpose

Redirect all emails to a test address during:

  • Development
  • Testing
  • Training

Configuration

UPDATE company_settings SET
  email_reroute_enabled = true,
  email_reroute_addresses = 'testing@company.com';

Behavior

When enabled:

  • Original recipients are ignored
  • All emails go to reroute address
  • Subject prefixed with "[REROUTED]"
  • Body includes original recipients

Error Handling

Non-Blocking Design

Email failures don't block the main operation:

async function submitLeaveRequest(request: LeaveRequest) {
  // 1. Save to database (critical)
  const { data, error } = await supabase
    .from('leave_requests')
    .insert(request);

  if (error) throw error; // This blocks

  // 2. Send notification (non-critical)
  try {
    await sendLeaveRequestNotification(data);
  } catch (emailError) {
    console.error('Email notification failed:', emailError);
    // Don't throw - request was saved successfully
  }

  return data;
}

Common Failures

Error Cause Solution
Authentication failed Wrong credentials Check username/password
Connection timeout Wrong host/port Verify SMTP settings
TLS error TLS mismatch Toggle TLS setting
Rate limited Too many emails Wait or upgrade plan
Spam blocked Content flagged Review email content

Monitoring

Edge Function Logs

View email logs in Supabase Dashboard:

  1. Go to Edge Functions
  2. Select send-notification-email
  3. Click Logs

Log Contents

{
  "timestamp": "2025-01-15T10:00:00Z",
  "status": "success",
  "recipients": ["user@company.com"],
  "subject": "Leave Request Approved",
  "rerouted": false
}

Debugging

Enable verbose logging in Edge Function:

console.log('SMTP Config:', {
  host: settings.smtp_host,
  port: settings.smtp_port,
  tls: settings.smtp_use_tls
});