Email

Transactional emails via Resend for password resets, invitations, and welcome messages.

Features

Setup

1. Create Resend Account

  1. Sign up at resend.com
  2. Add and verify your domain in Domains section
  3. Add the DNS records (SPF, DKIM, optionally DMARC)
💡 DNS Propagation

DNS changes usually complete within a few hours, but can take up to 48 hours.

2. Generate API Key

  1. Go to API Keys in Resend dashboard
  2. Click Create API Key
  3. Copy the key (you won't see it again)

3. Configure Environment

Add to backend/.env:

RESEND_API_KEY=re_your_api_key_here
EMAIL_FROM_ADDRESS=noreply@yourdomain.com
EMAIL_FROM_NAME=Your App Name
Variable Description Required
RESEND_API_KEY Your Resend API key Yes
EMAIL_FROM_ADDRESS Sender email (must be from verified domain) Yes
EMAIL_FROM_NAME Display name in email clients No
âš ī¸ Domain Verification Required

Emails will fail if EMAIL_FROM_ADDRESS uses an unverified domain.

Email Functions

All functions are in app/controllers/email.py:

send_email

Base function for sending any email:

from app.controllers.email import send_email

await send_email(
    to="user@example.com",
    subject="Hello!",
    html_body="<h1>Welcome</h1>"
)

send_password_reset_email

from app.controllers.email import send_password_reset_email

await send_password_reset_email(
    to="user@example.com",
    reset_url="https://yourapp.com/reset-password?token=abc123"
)

send_invitation_email

from app.controllers.email import send_invitation_email

await send_invitation_email(
    to="newuser@example.com",
    invite_url="https://yourapp.com/accept-invite?token=xyz789",
    invited_by="Admin User",
    organization_name="Acme Corp",  # Optional
    role_name="Editor"              # Optional
)

send_welcome_email

from app.controllers.email import send_welcome_email

await send_welcome_email(
    to="user@example.com",
    first_name="John",              # Optional
    organization_name="Acme Corp"   # Optional
)

Email Templates

All emails use responsive HTML with:

Email Type Subject Expiry Config
Password Reset "Reset your {APP_NAME} password" AUTH_PASSWORD_RESET_EXPIRE_HOURS (default: 24)
Invitation "You're invited to join {ORG} on {APP_NAME}" AUTH_INVITATION_EXPIRE_DAYS (default: 7)
Welcome "Welcome to {APP_NAME}!" N/A

Configuration

Optional settings in backend/.env:

# Password reset link validity (hours)
AUTH_PASSWORD_RESET_EXPIRE_HOURS=24

# Invitation link validity (days)
AUTH_INVITATION_EXPIRE_DAYS=7

Testing Locally

For development without domain verification:

  1. Use your Resend API key (test keys work)
  2. Set EMAIL_FROM_ADDRESS=onboarding@resend.dev
  3. Emails deliver to real addresses
â„šī¸ Resend Free Tier

3,000 emails/month and 100 emails/day - plenty for development and small apps.

Customizing Templates

Edit the HTML in app/controllers/email.py. Each function contains its template:

async def send_password_reset_email(to: str, reset_url: str) -> bool:
    subject = f"Reset your {settings.app_name} password"
    html_body = f"""
    <!DOCTYPE html>
    <html>
    <body>
        <h1>Reset Your Password</h1>
        <a href="{reset_url}">Reset Password</a>
    </body>
    </html>
    """
    return await send_email(to, subject, html_body)

Troubleshooting

Emails Not Sending

  1. Check configuration:

    from app.core.config import settings
    print(settings.email_is_configured)  # Should be True
    
  2. Verify domain in Resend dashboard - all DNS records should show green checkmarks

  3. Check logs:

    docker compose logs backend | grep -i email
    

Emails Going to Spam

Next Steps