Creating Customized i18n-Ready Authentication Emails using Supabase Edge Functions, PostgreSQL, and Resend

Welcome to another exciting tutorial on building robust applications! Today, we're delving into the world of Supabase, an open-source backend solution that equips developers with a powerful toolkit for creating scalable applications. In this guide, we'll explore how to harness the potential of Supabase Edge Functions, PostgreSQL, and the Resend email service to craft a personalized authentication email system.

Introduction

As the demand for efficient backend solutions continues to grow, Supabase has emerged as a game-changer. Offering features like real-time subscriptions, authentication, and database management, Supabase simplifies complex backend tasks. However, one unique challenge lies in customizing email templates for user engagement. In our previous blog post on Exploring Data Relationships with Supabase and PostgreSQL, we delved into the intricacies of data relationships. Building upon that foundation, we now tackle the challenge of creating a personalized authentication email system that also seamlessly supports i18n localization.

The Significance of i18n Localization in Emails

In today's global landscape, catering to diverse audiences is imperative for enhancing user engagement. Internationalization (i18n) ensures that emails resonate with users across various cultures and languages. By dynamically replacing content based on the user's preferred language, we not only enhance the user experience but also foster a sense of inclusivity.

Prerequisites

Before we dive into the technical intricacies, it's essential to be well-acquainted with Supabase, PostgreSQL, and have a grasp of the basics of Deno if necessary. Setting up your development environment, installing essential tools, and configuring dependencies based on your operating system will establish a solid foundation for a successful implementation.

Establishing a Solid Database Foundation

Our journey begins with the establishment of a robust database foundation. This involves the creation of an email_templates table within an internal schema. This table serves as a pivotal repository, storing vital information such as subject lines, content, languages, and template types.

CREATE SCHEMA internal;

CREATE TABLE internal.email_templates (
  id BIGINT GENERATED BY DEFAULT AS IDENTITY,
  subject TEXT NULL,
  content TEXT NULL,
  email_language TEXT NULL,
  email_type TEXT NULL,
  CONSTRAINT email_templates_pkey PRIMARY KEY (id)
);

By crafting this strong foundation, we lay the groundwork for a dynamic and versatile email system. This system will be powered by the forthcoming get_email_template function, which we'll explore in the following sections.

Crafting the Dynamic get_email_template Function

The core of our email system resides within the get_email_template function. This dynamic function retrieves email templates, utilizing inputs such as the template type, link, and language. Importantly, the function seamlessly integrates with the email_templates table, providing personalized email content.

CREATE OR REPLACE FUNCTION get_email_template(
  template_type TEXT,
  link TEXT,
  language TEXT DEFAULT 'en'
)
RETURNS JSON
SECURITY DEFINER
SET search_path = public, internal AS
$BODY$
DECLARE
  email_subject TEXT;
  email_content TEXT;
  email_json JSON;
BEGIN
  SELECT subject, REPLACE(content, '{{LINK}}', link) INTO email_subject, email_content
  FROM internal.email_templates
  WHERE email_type = template_type AND email_language = language;
  email_json := json_build_object('subject', email_subject, 'content', email_content);
  RETURN email_json;
END;
$BODY$
LANGUAGE plpgsql;
-- Protect this function to be only available to service_role key:
REVOKE EXECUTE ON FUNCTION get_email_template from public;
REVOKE EXECUTE ON FUNCTION get_email_template FROM anon, authenticated;

Customizing Email Templates for Common Authentication Scenarios

To enhance user experience and engagement, we'll be customizing email templates for several common authentication scenarios: password recovery, signup confirmation, invitation, and magic link.

These templates will be available in three languages: Portuguese, English, and Danish. You can seamlessly integrate these templates into the email_templates table within the internal schema. Below, you'll find the templates for each scenario:

--
-- Data for Name: email_templates; Type: TABLE DATA; Schema: internal; Owner: postgres
--

INSERT INTO "internal"."email_templates" ("id", "subject", "content", "email_language", "email_type") VALUES
    (1, 'Din Magisk Link', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<meta http-equiv="Content-Type" content="text/html charset=UTF-8" />
<html lang="da">
<head></head>
<body style="background-color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,&quot;Segoe UI&quot;,Roboto,Oxygen-Sans,Ubuntu,Cantarell,&quot;Helvetica Neue&quot;,sans-serif">
    <table align="center" role="presentation" cellSpacing="0" cellPadding="0" border="0" width="100%" style="max-width:37.5em;margin:0 auto;padding:20px 25px 48px;background-image:url(&quot;/assets/background-image.png&quot;);background-position:bottom;background-repeat:no-repeat, no-repeat">
        <tr style="width:100%">
            <td>
                <h1 style="font-size:28px;font-weight:bold;margin-top:48px">๐Ÿช„ Din magiske link</h1>
                <table style="margin:24px 0" align="center" border="0" cellPadding="0" cellSpacing="0" role="presentation" width="100%">
                    <tbody>
                        <tr>
                            <td>
                                <p style="font-size:16px;line-height:26px;margin:16px 0"><a target="_blank" style="color:#FF6363;text-decoration:none" href="{{LINK}}">๐Ÿ‘‰ Klik her for at logge ind ๐Ÿ‘ˆ</a></p>
                                <p style="font-size:16px;line-height:26px;margin:16px 0">Hvis du ikke har anmodet om dette, bedes du ignorere denne e-mail.</p>
                            </td>
                        </tr>
                    </tbody>
                </table>
                <p style="font-size:16px;line-height:26px;margin:16px 0">Bedste hilsner,<br />- Contoso Team</p>
                <hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#dddddd;margin-top:48px" />
                <p style="font-size:12px;line-height:24px;margin:16px 0;color:#8898aa;margin-left:4px">Contoso Technologies Inc.</p>
            </td>
        </tr>
    </table>
</body>
</html>
', 'da', 'magiclink');

-- Check the full SQL file and all templates in the GitHub repo
-- https://github.com/mansueli/Supabase-Edge-AuthMailer

By offering tailored templates for these scenarios in multiple languages, you ensure that your email communications resonate with a broader audience. This personalization fosters stronger user engagement and interaction.

Developing the Deno Edge Function (index.ts)

In the upcoming section, we'll delve into the development process of the Deno Edge Function responsible for managing authentication email requests. Below, we'll provide an overview of the pivotal steps involved in this process:

  1. Initializing Imports and Constants: We'll commence by importing essential modules and setting up constants that will facilitate the seamless development process.

  2. Creating a Secure Supabase Client: Learn how to establish a secure connection to Supabase using admin credentials, ensuring authorized access to the required resources.

  3. Handling Incoming HTTP Requests: Discover the utilization of the serve function to effectively handle incoming HTTP requests, enhancing the overall responsiveness of your system.

  4. Extracting Vital Parameters: Understand the process of extracting crucial parameters from incoming requests, such as email, authentication type, language, password, and redirection URL.

  5. Generating Secure Authentication Links: Explore the steps involved in generating secure authentication links using the Supabase admin API, facilitating secure user interactions.

  6. Customizing Redirection Links: Learn how to customize redirection links to match specific requirements and elevate the user experience.

  7. Invoking get_email_template Function: Dive into the usage of the Supabase rpc method to invoke the get_email_template function, enabling seamless retrieval of email content.

  8. Integration of Resend API: Understand the seamless integration of the Resend API, allowing the delivery of personalized and informative emails to users.

For the complete code of the Deno Edge Function, refer to the provided index.ts file.

// Importing required libraries
import { serve } from 'https://deno.land/std@0.192.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

// Defining CORS headers
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

// Log to indicate the function is up and running
console.log(`Function "auth-mailer" up and running!`)

// Creating a Supabase client using environment variables
const supabaseAdmin = createClient(
  Deno.env.get('SUPABASE_URL') ?? '',
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)

// Define a server that handles different types of requests
serve(async (req: Request) => {
  // Handling preflight CORS requests
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    // Destructuring request JSON and setting default values
    let { email, type, language = 'en', password = '', redirect_to = '' } = await req.json();
    console.log(JSON.stringify({ email, type, language, password }, null, 2));

    // Generate a link with admin API call
    let linkPayload: any = {
      type,
      email,
    }

    // If type is 'signup', add password to the payload
    if (type == 'signup') {
      linkPayload = {
        ...linkPayload,
        password,
      }
      console.log("linkPayload", linkPayload);
    }

    // Generate the link
    const { data: linkResponse, error: linkError } = await supabaseAdmin.auth.admin.generateLink(linkPayload)
    console.log("linkResponse", linkResponse);

    // Throw error if any occurs during link generation
    if (linkError) {
      throw linkError;
    }

    // Getting the actual link and manipulating the redirect link
    let actual_link = linkResponse.properties.action_link;
    if (redirect_to != '') {
      actual_link = actual_link.split('redirect_to=')[0];
      actual_link = actual_link + '&redirect_to=' + redirect_to;
    }

    // Log the template data
    console.log(JSON.stringify({ "template_type":type, "link": linkResponse, "language":language }, null, 2));

    // Get the email template
    const { data: templateData, error: templateError } = await supabaseAdmin.rpc('get_email_template', { "template_type":type, "link": actual_link, "language":language });

    // Throw error if any occurs during template fetching
    if (templateError) {
      throw templateError;
    }

    // Send the email using resend
    const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
    const resendRes = await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${RESEND_API_KEY}`,
      },
      body: JSON.stringify({
        from: 'rodrigo@mansueli.com',
        to: email,
        subject: templateData.subject,
        html: templateData.content,
      }),
    });

    // Handle the response from the resend request
    const resendData = await resendRes.json();
    return new Response(JSON.stringify(resendData), {
      status: resendRes.status,
      headers: {
        'Content-Type': 'application/json',
      },
    })
  } catch (error) {
    // Handle any other errors
    return new Response(JSON.stringify({ error: error.message }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 400,
    })
  }
})

Addressing Supabase Email Template Behavior

It's essential to acknowledge a particular behavior of Supabase when handling email templates. By default, Supabase removes HTML tags from email templates when using the platform's default templates. The approach we present here allows you to exercise control over the visual presentation of your email content. To effectively include HTML elements in your email templates, you can adhere to standard HTML practices.

Conclusion and Future Enhancements

Congratulations on successfully constructing a personalized authentication email system using Supabase Edge Functions, PostgreSQL, and the Resend service. By harnessing the capabilities of Supabase, you've streamlined the process of delivering tailored authentication emails to your users.

While this tutorial covers the foundational aspects, there's always room for growth. Consider expanding the range of email template customization or integrating additional third-party services to elevate user engagement. As you continue exploring Supabase's capabilities, share your insights and enhancements with the open-source community.

You can find the complete code used in this article on GitHub.

Further Learning Resources

For additional learning, we recommend exploring the following resources:

1
Subscribe to my newsletter

Read articles from Rodrigo Mansueli directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rodrigo Mansueli
Rodrigo Mansueli

Support Engineer @Supabase | StackOverflow