How to send welcome emails with Supabase edge functions and database triggers

Email is one of the best ways to engage with the users of your application, helping you establish strong connections, drive user retention, and generate more sales. You can send emails to onboard users, send notifications, promote special deals, gather feedback, announce events and so much more. When the decision to send these emails hinges on database events, triggers become essential.

What are database triggers?

Database triggers work like event listeners operating within your database. A trigger executes when the database is manipulated, for instance, when data is added, updated, or deleted. Triggers can run before or after an event has occurred.

This guide will walk you through the process of using Supabase database triggers to automatically send welcome emails to newly registered users. The trigger function will listen for the 'insert' event within the user's table and invoke an edge function that uses the SendGrid API to send the email.

Prerequisites

  • Node.js and npm are installed on your development machine.
  • A code editor like Vs Code.
  • A good understanding of Next.js.

Setting up Supabase

Supabase is an open-source Firebase alternative that simplifies database management. It offers a wide range of features, such as a Postgres database, authentication, edge functions, storage, and real-time data collaboration.

To get started, follow these instructions to set up Supabase:

  • Visit the Supabase website and sign up for an account if you haven't already.
  • Create a new project.
    • After signing in, click on Start Project.
    • Name your project, enter a password for the database, and choose your preferred region for hosting.
  • On the next screen, copy the anon key and the project URL from the Project API keys and Project Configuration sections respectively. We’ll use them later to connect to Supabase.

You don’t need to create a user table manually. Since we’re using the Supabase Auth to authenticate users, Supabase will create the users table in the auth schema when users register.

[@portabletext/react] Unknown block type "message", specify a component for it in the `components.types` prop

Authenticating users with Supabase Auth

Supabase provides an authentication service for authenticating and authorizing users in several ways including email/password, magic links, social providers, and phone numbers.

In this guide, we’ll use email/password to register new users in a Next.js application.

Create a Next.js application

We will use Next.js to create a simple sign-up form. To start, run the following command in your working directory to create a new Next.js project.

npx create-next-app send-welcome-email

This command will create a new folder called send-welcome-email with all the necessary files to get started with Next.js.

Navigate into this folder, and start the development server.

npm run dev

You can access your project by opening the web browser and navigating to http://localhost:3000.

Enable email authentication provider

On the dashboard sidebar of your Supabase project, click on the Authentication tab. In the Configuration section, select the Providers tab and enable the Email provider.

Configure Supabase in Next.js

Supabase provides the Next.js Auth Helpers package that simplifies authentication in Next.js across server and client components.

Install the Next.js Auth Helpers library in your application.

npm install @supabase/auth-helpers-nextjs @supabase/supabase-js

Declare environment variables

Copy the project URL and anon key from your API settings, and create a .env file with the following environment variables:

NEXT_PUBLIC_SUPABASE_URL=your-supabase-url

NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key

Instantiate a Supabase client

Create a new folder named lib and add a new file supabase.js to it. In this file, use the Supabase URL and the anon key to create a client component instance.

import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
export const supabaseClient = createClientComponentClient(
  supabaseUrl,
  supabaseKey
);

You can now import the Supbase client across your application and use it to authenticate users.

Create a registration form

In the app directory, create a new folder named sign up and add a page.jsx file. In this file, add the following code to create the sign up form.

"use client";
import { useState } from "react";

function Page() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  const handlePassword = (e) => {
    setPassword(e.target.value);
  };
  const handleEmail = (e) => {
    setEmail(e.target.value);
  };
  return (
    <form onSubmit={handleSignUp}>
      <input
        type="email"
        name="Email"
        placeholder="Enter your email"
        aria-label="Email"
        value={email}
        onChange={handleEmail}
      />
      <input
        type="password"
        name="Password"
        placeholder="Enter your password"
        aria-label="Password"
        value={password}
        onChange={handlePassword}
      />
      <button type="Submit">Sign Up With Email</button>
    </form>
  );
}
export default Page;

The sign up form comprises an email and password input box and a submit button. The form has an onSubmit handler that listens for submit events and calls a function called handleSignUp function.

This function calls the signUp method from Supabase with the email and password as arguments and returns data and an error object.

Before creating it, import the Supabase client from lib/supabase at the top of signup/page.jsx.

import { supabaseClient } from "@/lib/supabaseClient"

Then, add the handleSignUp function to the component.

const handleSignUp = async () => {
  try {
    const { data, error } = await supabaseClient.auth.signUp({
      email,
      password,
    });
    if (error) throw Error();
    console.log(data);
  } catch (error) {
    console.log(error.message);
  }
};

If an error occurs, the function will throw an error. Otherwise it creates a new user in the database.

Visit the signup page to test out the authentication process. If it works, you should see the user information logged in the console. You can also view the authenticated users by clicking the Authentication section in the left menu of your project’s dashboard.

Creating an edge function to send emails

Supabase edge functions are server-side Typescript functions distributed globally close to the location where they are consumed. They are usually small, lightweight pieces of code that perform specific tasks like sending emails.

To get started with edge functions, set up a Supabase project in your local development environment, by following these steps:

  • Install the Supabase CLI.
npm i supabase --save-dev
  • Log in to the CLI.
supabase login

Supabase will prompt you for an access token. Get the access tokens from your Supabase dashboard.

  • Create a new folder called send-email-edge and navigate to it. This is where you’ll create your function.
  • Initialize Supabase inside the folder.
supabase init
  • Link to your Supabase project.
supabase link --project-ref <your-project-ref>

You can get <your-project-ref> from your project's dashboard URL: https://supabase.com/dashboard/project/<your-project-ref>.

  • Supabase edge functions are developed using Deno so follow the official Deno guide to set up Deno in your preferred IDE.

Once you've set up Deno, create a Supabase edge function:

supabase functions new send-welcome-email

This command will create a new function with boilerplate code to get you started.

Replace the contents within the send-welcome-email/index.js file with the following code. This code includes a try/catch block responsible for extracting an email from the request body and generating appropriate responses for both success and error scenarios.

serve(async (req) => {
  try {
    const { email } = await req.json();

    return new Response(JSON.stringify({ message: "Success", error: null }), {
      status: 200,
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    return new Response(
      JSON.stringify({ message: "Error", error: error.message }),
      {
        headers: { "Content-Type": "application/json" },
        status: 400,
      },
    );
  }
});

To send the emails, we will use the Email API provided by SendGrid. You can, however, use whichever email provider you prefer. Note that Supabase blocks connections on ports 587 and 465 therefore, use a provider that has an API or lets you customize the port.

[@portabletext/react] Unknown block type "message", specify a component for it in the `components.types` prop

Setting up SendGrid

There are a couple of things you need to do before using SendGrid API:

To send emails, you will need to verify ownership of the email address you want to use.

In the left menu, click on the Settings tab, then select Sender Authentication. Click Verify a Single Sender and follow the instructions to add one of your emails as the sender's email.

  • Get the API key.

Under Settings, click API keys and then click the Create the API Key button.

Give your API key a proper name like handle-new-user. Under API key permissions, choose restricted access and enable full access for the mail send option. Click Create and View to finish the process.

Copy the API key and create a .env file in your Supabase project folder to store it securely. Make sure to use your actual API key.

SENDGRID_API_KEY="your-sendgrid-api-key"

You will also need to define the SendGrid API endpoint URL, your email, and your name.

SENDGRID_ENDPOINT= '<https://api.sendgrid.com/v3/mail/send>'

SENDGRID_SENDER_EMAIL="your-email"

SENDGRID_SENDER_NAME="your-name"

Push the variables from the .env file to your remote project using:

supabase secrets set --env-file ./supabase/.env

The edge function can now access the environment variables when you deploy it.

Send a welcome email using the email API

First, retrieve the SendGrid API key, endpoint URL, sender email, and sender name from the .env file using Deno:

const SENDGRID_API_KEY = Deno.env.get('SENDGRID_API_KEY')

const SENDGRID_ENDPOINT = Deno.env.get('SENDGRID_ENDPOINT')

const SENDGRID_SENDER_EMAIL = Deno.env.get('SENDGRID_SENDER_EMAIL')

const SENDGRID_SENDER_NAME = Deno.env.get('SENDGRID_SENDER_NAME')

Next, define the email data by adding the following data object to your function. It specifies the sender’s email (to), the subject of the email, its contents, and the email’s recipient (from).

const data = {
  personalizations: [
    {
      to: [
        {
          email: email,
        },
      ],
      subject: "Welcome!",
    },
  ],
  content: [
    {
      type: "text/plain",
      value: "Thanks for registering!",
    },
  ],
  from: {
    email: `${SENDGRID_SENDER_EMAIL}`,
    name: `${SENDGRID_SENDER_NAME}`,
  },
};

The SendGrid API requires the authorization header, which contains the API key you created and the content-type header. Define them by adding the following to the function.

const headers = {
  Authorization: `Bearer ${SENDGRID_API_KEY}`,
  'Content-Type': 'application/json'
};

Then, use fetch to send a POST request c containing the email information to SendGrid.

if (email && SENDGRID_ENDPOINT) {
  const response = await fetch(SENDGRID_ENDPOINT, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });
  if (!response.ok) throw Error("Could not send email");
}

If the fetch request is unsuccessful, it should throw an error.

Altogether, the edge function should look like this:

const SENDGRID_API_KEY = Deno.env.get("SENDGRID_API_KEY");
const SENDGRID_ENDPOINT = Deno.env.get("SENDGRID_ENDPOINT");
const SENDGRID_SENDER_EMAIL = Deno.env.get("SENDGRID_SENDER_EMAIL");
const SENDGRID_SENDER_NAME = Deno.env.get("SENDGRID_SENDER_NAME");

import { serve } from "https://deno.land/std@0.177.0/http/server.ts";

serve(async (req: Request) => {
  try {
    const { email } = await req.json();

    const data = {
      personalizations: [
        {
          to: [
            {
              email: email,
            },
          ],
          subject: "Welcome!",
        },
      ],
      content: [
        {
          type: "text/plain",
          value: "Thanks for registering!",
        },
      ],
      from: {
        email: SENDGRID_SENDER_EMAIL,
        name: SENDGRID_SENDER_NAME,
      },
    };

    const headers = {
      Authorization: `Bearer ${SENDGRID_API_KEY}`,
      "Content-Type": "application/json",
    };

    if (email && SENDGRID_ENDPOINT) {
      const response = await fetch(SENDGRID_ENDPOINT, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(data),
      });
      if (!response.ok) throw Error("Could not send email");
    }

    return new Response(JSON.stringify({ message: "Success", error: null }), {
      status: 200,
      headers: {
        "Content-Type": "application/json",
      },
    });
  } catch (error) {
    return new Response(
      JSON.stringify({ message: "Error", error: error.message }),
      {
        headers: { "Content-Type": "application/json" },
        status: 400,
      },
    );
  }
});

Run this command in the terminal to deploy the edge function.

supabase functions deploy

In your project dashboard, click on edge functions in the left menu to view your function. From here, you can view information about the function including logs which come in handy during debugging.

To invoke the function, use a REST client like Postman or use the following curl command.

curl -L -X POST 'edge-function-URL' -H 'Authorization: Bearer [YOUR ANON KEY]' --data '{"email":"recipient-email"}'

The function should send the welcome email to the recipient email you specify.

We don’t want to invoke the function manually, we want to create a trigger function that automatically calls this function when a new user signs up.

[@portabletext/react] Unknown block type "newsletter", specify a component for it in the `components.types` prop

Create a trigger function to call the edge function

Follow the instructions below to create a Trigger Function in Supabase’s SQL editor:

  • In the left menu of your project’s dashboard, click on the SQL editor tab.
  • Click New Query and add the following function.
create or replace function handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
    PERFORM net.http_post(
      url := '[edge-function-url]',
      headers := '{"Content-Type": "application/json", "Authorization": "Bearer [your-anon-key]"}'::jsonb,
      body:=concat('{"email": "', NEW.email, '"}')::jsonb
    );
    return new;
end;
$$;

This function is called handle_new_user. It uses the PG_NET extension to send an HTTP POST request containing the email of the user signing up to the edge function.

  • Enable PG_NET extension by running the statement below in the SQL editor.
create extension pg_net;

This extension allows you to invoke the edge function using HTTP asynchronously.

  • Create a trigger that calls the handle_new_user function when a new row is inserted in the users table by running this statement in the SQL command.
create trigger handle_new_user_trigger
after insert on auth.users for each row
execute function handle_new_user ();
  • Click run to execute the statement.

Now, if a new user signs up, they should receive a welcome message in their email.

Conclusion

In this article, you learned how you can use edge functions and database triggers to send emails to newly registered users.  Aside from sending welcome emails, you can tweak the trigger and email content to do other things too. For instance, you could send confirmation emails to users after they make a purchase or sign up for an event. And it's not just about emails. You can also perform actions such as calculations before adding data to a table. The possibilities are endless.

Find the code for the edge function and the code for authenticating users on GitHub.