# Email providers (Resend, Postmark, SendGrid)

> Send your app's own emails from SupaHost with Resend, Postmark or SendGrid through an Edge Function, trigger them from the database, and understand how this differs from Auth emails.

Source: https://supahost.app/docs/integrations/resend · Updated 2026-11-11

There are two kinds of email in a Supabase app, and on SupaHost they're handled differently:

| Kind | Examples | Who sends it |
|---|---|---|
| Auth emails | Confirm sign-up, magic link, password reset, invite, change email | SupaHost, from `noreply@supahost.app` with your App name as the sender name |
| App emails | Booking confirmations, receipts, reminders, digests | You, through a provider such as Resend, Postmark or SendGrid |

Custom SMTP for Auth emails isn't available on SupaHost, so you can't route Auth emails through your own provider account. You can change their wording on the project's **Auth** tab, in the **Auth emails** card; see [Auth emails and templates](/docs/auth/email-templates). Everything else your app sends goes through your provider, usually from an Edge Function.

## Why an Edge Function

Provider API keys are secrets. They can't be in browser or mobile code, so the sending happens on a server. An Edge Function is the closest server you have: it runs on your instance, can read the provider key from function secrets, and can check who is asking.

## 1. Add the provider key

On the project's **Functions** tab, in the **Secrets** card, add your provider's key, for example `RESEND_API_KEY`, `POSTMARK_SERVER_TOKEN` or `SENDGRID_API_KEY`. Verify your sending domain with the provider first, or messages will be rejected or land in spam.

## 2. Write the function

This function sends a booking confirmation for the signed-in user. Keep **Require JWT** on.

```ts
// send-booking-email/index.ts
import { createClient } from 'npm:@supabase/supabase-js@2'

Deno.serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
  )

  const token = (req.headers.get('Authorization') ?? '').replace('Bearer ', '')
  const { data: { user } } = await supabase.auth.getUser(token)
  if (!user?.email) return new Response('Unauthorized', { status: 401 })

  const { bookingId } = await req.json()

  // Runs as the user, so RLS stops them reading someone else's booking.
  const { data: booking, error } = await supabase
    .from('bookings')
    .select('id, room, starts_at, nights')
    .eq('id', bookingId)
    .single()
  if (error) return new Response('Booking not found', { status: 404 })

  const res = await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'Harbour Inn <bookings@harbourinn.example>',
      to: [user.email],
      subject: `Your booking for room ${booking.room}`,
      html: `<p>See you on ${new Date(booking.starts_at).toDateString()} for ${booking.nights} nights.</p>`,
    }),
  })

  if (!res.ok) return new Response(await res.text(), { status: 502 })
  return Response.json({ sent: true })
})
```

Deploy it from the Functions tab and call it from your app:

```ts
await supabase.functions.invoke('send-booking-email', { body: { bookingId: 42 } })
```

## Other providers

Only the `fetch` call changes. Postmark:

```ts
await fetch('https://api.postmarkapp.com/email', {
  method: 'POST',
  headers: {
    'X-Postmark-Server-Token': Deno.env.get('POSTMARK_SERVER_TOKEN')!,
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify({
    From: 'bookings@harbourinn.example',
    To: user.email,
    Subject: 'Your booking',
    HtmlBody: '<p>Thanks for booking.</p>',
    MessageStream: 'outbound',
  }),
})
```

SendGrid:

```ts
await fetch('https://api.sendgrid.com/v3/mail/send', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${Deno.env.get('SENDGRID_API_KEY')}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    personalizations: [{ to: [{ email: user.email }] }],
    from: { email: 'bookings@harbourinn.example' },
    subject: 'Your booking',
    content: [{ type: 'text/html', value: '<p>Thanks for booking.</p>' }],
  }),
})
```

Each provider also has an npm SDK you can import with `npm:`; plain `fetch` keeps the function small.

## Sending when data changes

To email when a row is inserted, without the app calling anything, let the database call your function with `pg_net`, which is installed in every SupaHost project. Turn **Require JWT** off for that function and check a shared secret instead:

```sql
create or replace function public.email_new_booking()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  perform net.http_post(
    url := 'https://app-one.acme.supahost.space/functions/v1/booking-created',
    body := jsonb_build_object('booking_id', new.id),
    headers := jsonb_build_object(
      'Content-Type', 'application/json',
      'x-webhook-secret', (select decrypted_secret from vault.decrypted_secrets where name = 'booking_webhook_secret')
    )
  );
  return new;
end;
$$;

create trigger bookings_send_email
  after insert on public.bookings
  for each row execute function public.email_new_booking();
```

Store the secret once with `select vault.create_secret('<random value>', 'booking_webhook_secret');`, and add the same value as a function secret so the function can compare it. `pg_net` sends the request in the background after the transaction commits, so a slow provider doesn't slow down the insert. Inside the function, use the `service_role` key to look up the booking and the guest's email.

For reminders and digests, schedule a function call with [pg_cron](/docs/cron-queues/cron) instead.

## Related

- [Example: sending email](/docs/functions/sending-email)
- [Database webhooks and HTTP requests with pg_net](/docs/database/webhooks)
- [Not receiving Auth emails](/kb/not-receiving-auth-emails)
- [Sending Auth emails in the user's language](/kb/custom-emails-by-language)
