# Connect Supabase to SendBeam

How to connect Supabase to SendBeam: what you need, the setup steps, and the code.

Via automation

Supabase gives you a Postgres database with triggers and outbound HTTP built in. That is everything needed to send email from the data itself — an order row appears and the receipt goes out, with no worker to deploy, no queue to run and nothing between your database and the send.

## Before you start

- A Supabase project
- A verified sending domain in SendBeam
- A SendBeam API key with transactional:send

## Set it up

1. **Verify a sending domain.** Settings → Domains. Transactional mail goes out over your own domain, so this has to be done first — the send is refused without it.
2. **Create a scoped API key.** Settings → API keys, with `transactional:send` and nothing else. That permission sends to any address, which is the point: a customer getting a receipt is not a contact and does not have to be one. Do not reuse a key that also carries `contacts:write` or `campaigns:write` — this one lives in your database, and the whole reason to scope it is the day it leaks.
3. **Put the key in Vault, not in the trigger.** Supabase Vault encrypts it at rest and keeps it out of `pg_dump` output, migrations and your repository.
4. **Enable `pg_net`.** Database → Extensions. It is what lets Postgres make an outbound request.
5. **Add the trigger.** The example below sends a receipt when a row is inserted into `orders`.
6. **Check it worked.** Insert a row and look at SendBeam → the workspace’s activity, then `select * from net._http_response order by created desc limit 5` in Supabase for the response we sent back.

## Code

### Store the key

`SQL editor, once`

```
select vault.create_secret('sb_live_your_key_here', 'sendbeam_api_key');
```

### Send a receipt when an order is created

`SQL editor`

```
create or replace function public.send_order_receipt()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
  v_key text;
begin
  select decrypted_secret into v_key
    from vault.decrypted_secrets
   where name = 'sendbeam_api_key';

  perform net.http_post(
    url     := 'https://sendbeam.io/api/v1/transactional',
    headers := jsonb_build_object(
                 'Content-Type', 'application/json',
                 'x-api-key', v_key
               ),
    body    := jsonb_build_object(
                 'to',      new.customer_email,
                 'subject', 'Your order ' || new.reference,
                 'html',    '<p>Thanks for your order.</p>'
                            || '<p>Reference: <strong>' || new.reference || '</strong><br>'
                            || 'Total: ' || to_char(new.total, 'FM999,999.00') || '</p>'
               )
  );

  return new;
end;
$$;

create trigger orders_send_receipt
  after insert on public.orders
  for each row execute function public.send_order_receipt();
```

`net.http_post` returns immediately with a request id and the request is made in the background, so the insert is never held up waiting for us and never fails because we were slow.

### Map the row explicitly

The three fields above are the whole contract: `to`, `subject`, and `html` or `text`. Everything else is optional — `replyTo`, a `from` of `{ "name": ..., "email": ... }`, and up to ten custom `headers`. Build them from the row the same way:

```
body := jsonb_build_object(
  'to',      jsonb_build_array(jsonb_build_object('email', new.customer_email, 'name', new.customer_name)),
  'subject', 'Your order ' || new.reference,
  'text',    'Thanks for your order. Reference: ' || new.reference,
  'replyTo', 'support@yourdomain.test',
  'headers', jsonb_build_object('X-Order-Reference', new.reference)
)
```

Send `text` on its own and we build the HTML from it, which is usually the right choice for a receipt.

## Things to know

- **The Database Webhooks UI will not do this.** It is worth being clear, because it looks like it should. That screen sends a fixed envelope — `{ "type": "INSERT", "table": "orders", "record": { ... } }` — and lets you add headers but not change the body. `/api/v1/transactional` needs `to`, `subject` and a body, so the envelope is rejected as a bad request. Writing the trigger yourself, as above, is what lets you decide the payload. If you would rather use the UI, put a Supabase Edge Function in between to reshape the envelope, and point the webhook at that.
- **A Supabase retry is not a SendBeam retry.** `pg_net` does not retry. If the request fails, it fails once and the row is already committed — the trigger fired `after insert`, so nothing is rolled back and nobody is told. For mail that genuinely must arrive, write the send to a table, retry from a scheduled job, and mark the row when we return a 2xx. Reading `net._http_response` is how you find out what happened.
- **We refuse some addresses on purpose.** An address that has hard bounced or reported a previous email as spam is not sent to again, and the response says which and why. Unsubscribed is different: someone who opted out of marketing still gets their receipt, because the opt-out does not cover mail they asked for.
- **Limits.** Ten recipients per request, a 500-character subject, and 500 KB of body. Over the hourly send allowance for your plan you get a `429` with a `Retry-After` header rather than a silent drop.
- **`security definer` is doing real work here.** The function reads Vault, which the role performing the insert cannot. Pin `search_path`, as above, so the function resolves table names in a schema you control rather than one the caller chose.

[← Supabase email marketing integration](https://sendbeam.io/integrations/supabase)

---
Source: https://sendbeam.io/docs/integrations/supabase
