# Example: sending email

> Send transactional email from a SupaHost Edge Function with an email API such as Resend, safely tied to the signed-in user, and trigger it from your app or the database.

Source: https://supahost.app/docs/functions/sending-email · Updated 2026-11-11

SupaHost sends your project's Auth emails (sign-up confirmation, magic links, password resets) for you, using the templates on the **Auth** tab. Custom SMTP isn't available for those. For your own app's email, such as receipts, reminders and notifications, call an email provider's HTTP API from an Edge Function. This page uses [Resend](https://resend.com); Postmark, SendGrid and others work the same way with their own endpoints.

The function below sends a booking confirmation to the signed-in user. It only ever emails the caller about their own booking, so it can't be abused to send arbitrary mail.

## 1. Set up the provider

1. Create a Resend account and verify a sending domain (for example `mail.example.com`) by adding the DNS records Resend gives you. Until a domain is verified you can only send test mail.
2. Create an API key with sending access.
3. In *Dashboard → your project → **Functions** tab → **Secrets***, add `RESEND_API_KEY` with that key.

## 2. The function

Create a function named `send-booking-confirmation` and keep **Require a JWT** on.

```ts
import { createClient } from "jsr:@supabase/supabase-js@2";

const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY")!;
const FROM = "Harbour Rooms <bookings@mail.example.com>";

const corsHeaders = {
  "Access-Control-Allow-Origin": "https://bookings.example.com",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

function escapeHtml(value: string) {
  return value.replace(/[&<>"']/g, (c) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!,
  );
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

  const authHeader = req.headers.get("Authorization") ?? "";
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    { global: { headers: { Authorization: authHeader } } },
  );

  // 1. Who is calling? The anon key alone isn't enough.
  const { data: { user } } = await supabase.auth.getUser(authHeader.replace(/^Bearer\s+/i, ""));
  if (!user?.email) {
    return Response.json({ error: "Sign in first" }, { status: 401, headers: corsHeaders });
  }

  // 2. Load the booking as that user. RLS returns nothing if it isn't theirs.
  const { bookingId } = await req.json().catch(() => ({ bookingId: null }));
  const { data: booking, error } = await supabase
    .from("bookings")
    .select("id, room, starts_at")
    .eq("id", bookingId)
    .maybeSingle();
  if (error || !booking) {
    return Response.json({ error: "Booking not found" }, { status: 404, headers: corsHeaders });
  }

  // 3. Build the message on the server, from trusted data.
  const when = new Date(booking.starts_at).toUTCString();
  const room = escapeHtml(booking.room);
  const res = await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${RESEND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      from: FROM,
      to: [user.email],
      subject: `Your booking: ${booking.room}`,
      html: `<p>Thanks for booking <strong>${room}</strong>.</p><p>Check-in: ${when}</p>`,
      text: `Thanks for booking ${booking.room}.\nCheck-in: ${when}`,
    }),
  });

  if (!res.ok) {
    console.error("Resend error", res.status, await res.text());
    return Response.json({ error: "Couldn't send the email" }, { status: 502, headers: corsHeaders });
  }

  const { id } = await res.json();
  console.log(`Sent confirmation ${id} for booking ${booking.id}`);
  return Response.json({ sent: true, id }, { headers: corsHeaders });
});
```

Deploy it, then call it from your app after the booking is saved:

```ts
const { data, error } = await supabase.functions.invoke("send-booking-confirmation", {
  body: { bookingId: booking.id },
});
```

## Sending from the database instead

Sometimes the email should go out whenever a row appears, whatever client created it. Two options:

- **Database webhook.** Add a trigger that calls the function with `net.http_post` on insert. Send the `service_role` key (kept in Vault) as the bearer token, and have the function check the role and look the booking up with an admin client. See [Database webhooks and HTTP requests with pg_net](/docs/database/webhooks).
- **Queue.** Insert a message into a pgmq queue on insert, and have a scheduled function send emails in batches. This survives provider outages, because unsent messages stay in the queue and reappear after their visibility timeout. See [Queues with pgmq](/docs/cron-queues/queues) and [Processing queue messages](/docs/cron-queues/queue-consumers).

For scheduled digests and reminders, see [Running functions on a schedule](/docs/functions/scheduling).

## Good habits

- **Never let callers choose the recipient or the content** of mail sent from your domain. Derive both from the signed-in user and your own data.
- **Escape anything you put into HTML**, even values from your own database.
- **Send a plain-text part** alongside the HTML; it helps deliverability.
- **Log the provider's message id**, not the email body. The Logs card is readable by your whole team.
- **Keep it fast.** The provider usually answers in well under a second, but if you send many messages per request, use a queue so you stay well inside the 150-second response limit.

## Other providers

Swap the `fetch` call for the provider's API: Postmark takes a `X-Postmark-Server-Token` header at `https://api.postmarkapp.com/email`, and SendGrid a bearer token at `https://api.sendgrid.com/v3/mail/send`, each with its own JSON shape. See [Email providers (Resend, Postmark, SendGrid)](/docs/integrations/resend).

## Related

- [Auth emails and templates](/docs/auth/email-templates)
- [Environment variables and secrets](/docs/functions/secrets)
- [Authentication and JWT verification](/docs/functions/auth)
