Go Back

Transactional emails with Supabase Edge Functions and Lemon Squeezy

Learn how to set up transactional emails for your product using Lemon Squeezy and Supabase Edge Functions.

When one of your customers buys a product or a subscription ends, you will probably want to send an email. But the workflow for doing so is not always obvious.

In this article, I will show you how to set up transactional emails for your product using Lemon Squeezy and the Edge Functions from Supabase (which can now be self-hosted since their 7th launch week ๐Ÿ”ฅ).

๐Ÿ’ก I use MailerSend to send my emails, but you can use your favorite tool. This tutorial is also easily transferable to any payment platform (Stripe, Gumroad, Paddle...) with webhooks.

Prerequisites

Step 1: Initialize Supabase

If you already have Supabase initialized inside your project (i.e., you have a supabase folder and the CLI installed), skip this section.

Otherwise, log in to your Supabase account using the following command:

supabase login

Supabase CLI reference for this command

Youโ€™ll need an access token, which you can generate through your Supabase Dashboard by going to the โ€œAccess Tokensโ€ page.

Once youโ€™ve logged in, initialize Supabase inside your project with the following command:

supabase init

Supabase Documentation for this command

Bravo, now you should have a supabase folder inside your project ๐Ÿฅณ

Step 2: Create your first Edge Function

The last step is very straightforward. Simply enter this command to create your first Edge Function:

supabase functions new purchase-emails

You should have something similar to this in your project folder:


๐Ÿ’ก Supabase Edge Functions use Deno. If you want to integrate the Deno language server with your editor, follow these steps.


Next, letโ€™s write a simple function, deploy it, and check that it works.

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

console.log('hello toto')

serve(async (req: any) => {
  return new Response(
    JSON.stringify({
      message: 'Success!'
    }),
    { headers: { "Content-Type": "application/json" } }
  )
})

Deploy the function by running this command:

supabase functions deploy purchase-emails

๐Ÿคฉย And the magic happened! Go to your Supabase dashboard, in the "Edge Functions" part of your project. You should see your function:

Click on it to access a VERY useful page: you have access to a lot of details about your function, live logs, metrics an invocations. Incredibly helpful when you need to debug!

๐Ÿ’ก Since the 7th Launch week of Supabase, you can Self host your functions! [Click here to read about it](https://supabase.com/blog/edge-runtime-self-hosted-deno-functions)

Step 3: Sending a Webhook to your function

If you're using Lemon Squeezy, go to your dashboard and then navigate to Settings > Webhooks. Click on the โ€œ+โ€ button to create your first webhook.

In the โ€œCallback URLโ€ field, enter your Supabase Edge Function URL, which you can find on its details page. It will look something like this: https://xxxxxxxxxxxxxxx.functions.supabase.co/purchase-emails

Enter a random value in the โ€œSigning secretโ€ field, and check the updates you want to send a webhook for. In this tutorial, weโ€™ll use the order_created update.

Save your webhook.

There are two more things you need to know:

  • Lemon Squeezy has a test mode, which you can activate at the bottom left of your screen when you are on your dashboard. This is very useful for testing your webhook. Don't forget to read the documentation to learn more about it.
  • Once you have sent your first webhook, a preview of the most recent submissions will be displayed on your Lemon Squeezy dashboard, in the Webhooks section.

Lemon Squeezy webhooks section with the latest webhooks deliveries

Step 4: Receiving your Webhooks and sending your emails

Finally, we will write the code that will allow us to retrieve the information sent by our webhook and send a purchase confirmation email to the user.

To retrieve the webhook data, simply add this line at the beginning of our serve function:

const event = await req.json()

The event constant will contain all the information sent by Lemon Squeezy. Refer to their documentation to see what this data looks like.

In this tutorial, weโ€™ll need the customer email, which is located in data.attributes.user_email . We just have to make our API call to our email sending provider with the user's email.

Here's an example using MailerSend and a template:

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

serve(async (req: any) => {
  const event = await req.json()
  try {
    await fetch(`https://api.mailersend.com/v1/email`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${Deno.env.get("MAILERSEND_TOKEN")}`
      },
      body: JSON.stringify({
        "from": {
          "email": "contact@uneed.best",
          "name": "Uneed"
        },
        "to": [
          {
            "email": event.data.attributes.user_email,
            "name": event.data.attributes.user_email
          },
        ],
        "subject": "Uneed - Your Skip the waiting line purchase",
        "template_id": "xxxxxxxxx",
      })
    })

    return new Response(
      JSON.stringify({
        message: 'Success!'
      }),
      { headers: { "Content-Type": "application/json" } }
    )

  } catch (e) {
    throw new Error('Error :/')
  }
})

If you want to verify the Lemon Squeezy signature, itโ€™s getting a bit more complicated:

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import * as crypto from "https://deno.land/std@0.168.0/node/crypto.ts"

serve(async (req: any) => {
  const rawBody = await req.text()
  const event = JSON.parse(rawBody)
  const signature = req.headers.get('X-Signature') || ''

  const hmac = crypto.createHmac('sha256', Deno.env.get("LS_SIGNATURE_KEY"))
  hmac.update(rawBody)

  const digest = hmac.digest('hex')

  if (signature !== digest) {
    throw new Error('Invalid signature.')
  } else {
      await fetch(`https://api.mailersend.com/v1/email`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${Deno.env.get("MAILERSEND_TOKEN")}`
        },
        body: JSON.stringify({
          "from": {
            "email": "contact@uneed.best",
            "name": "Uneed"
          },
          "to": [
            {
              "email": event.data.attributes.user_email,
              "name": event.data.attributes.user_email
            },
          ],
          "subject": "Uneed - Your Skip the waiting line purchase",
          "template_id": "xxxxxxxx",
        })
      })

      return new Response(
        JSON.stringify({
          message: 'Success!'
        }),
        { headers: { "Content-Type": "application/json" } }
      )
  }
})

And that's it! ๐Ÿ˜„ With this function, you can handle all of your product purchases. Supabase recommends "developing fat functions" in their documentation, which means that you should develop a few large functions instead of many small functions.

Be sure to add your product to Uneed and skip the waiting line to receive an email from a Supabase Edge Function ๐Ÿ‘€