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 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,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",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("/assets/background-image.png");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:
Initializing Imports and Constants: We'll commence by importing essential modules and setting up constants that will facilitate the seamless development process.
Creating a Secure Supabase Client: Learn how to establish a secure connection to Supabase using admin credentials, ensuring authorized access to the required resources.
Handling Incoming HTTP Requests: Discover the utilization of the
serve
function to effectively handle incoming HTTP requests, enhancing the overall responsiveness of your system.Extracting Vital Parameters: Understand the process of extracting crucial parameters from incoming requests, such as email, authentication type, language, password, and redirection URL.
Generating Secure Authentication Links: Explore the steps involved in generating secure authentication links using the Supabase admin API, facilitating secure user interactions.
Customizing Redirection Links: Learn how to customize redirection links to match specific requirements and elevate the user experience.
Invoking
get_email_template
Function: Dive into the usage of the Supabaserpc
method to invoke theget_email_template
function, enabling seamless retrieval of email content.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: