Supabase Edge Functions are a practical place to send transactional messages without exposing an email API key in a browser. This guide shows how to send email with Supabase Edge Functions using Volanea’s REST API, a server-side environment variable, and one copy-pasteable TypeScript function.

The integration deliberately uses the standard fetch API rather than an unverified provider-specific SDK. Supabase Edge Functions run on Deno, which includes fetch, Request, Response, Web Crypto, and environment-variable access. That means the function can call Volanea’s HTTPS API directly with no email package, no Node compatibility layer, and no API key shipped to the client.

What you will build

By the end of this guide, you will have a Supabase Edge Function that:

  • reads a Volanea secret API key from Deno.env;
  • reads the sender and test recipient from environment variables;
  • accepts a POST request;
  • sends one transactional email through POST https://api.volanea.com/v1/send;
  • returns Volanea’s API response and status code to the caller;
  • prevents accidental duplicate test sends with an Idempotency-Key; and
  • keeps the Volanea key out of your frontend source code and Git history.

The example sends a fixed welcome email so it is safe to test without accepting an arbitrary to address or arbitrary HTML from a public request. Once it works, you can adapt it for application events such as signups, receipts, invitations, password-reset notifications, security alerts, or billing failures.

Before sending, make sure the address used as VOLANEA_FROM_EMAIL belongs to a verified sending domain in Volanea. A valid-looking From address alone is not enough: the domain must be configured for authenticated delivery before production recipients should receive mail.

Why use an Edge Function for transactional email

A browser should never call an email provider with a secret sending key. If the key is embedded in a client-side bundle, a user can retrieve it from the browser, replay requests, and send messages under your account. Even if your app has authentication, exposing the provider credential turns every authenticated client into a potential email-sending machine.

An Edge Function changes that boundary. The browser calls your Supabase endpoint, Supabase handles the function invocation, and the function makes the outbound request to Volanea. The Volanea API key stays in Supabase secrets and is available only while the server-side function is running.

This architecture also gives your application a place to enforce business rules before sending. For example, your function can verify the current user, look up an order from Postgres, choose the recipient from a trusted database record, and construct the email from server-owned fields. The caller should request an action such as “send my receipt,” not provide a free-form sender, recipient, subject, and HTML payload.

When an Edge Function is the right fit

Use this pattern when an event needs an immediate, one-to-one email response. Typical examples include:

  • A user signs up and needs a welcome or verification message.
  • A customer completes a purchase and needs an order confirmation.
  • A workspace owner invites a teammate.
  • A payment attempt fails and the account owner needs an alert.
  • A user requests an export and needs a secure download notification.
  • An administrator triggers a support, account, or security workflow.

For high-volume broadcasts, do not invoke an Edge Function once for every recipient from a browser loop. Instead, use a server-owned job or campaign workflow that can process recipients in controlled batches, obey rate limits, and record outcomes. Transactional functions should be short-lived, narrowly scoped, and tied to one application event.

Why this guide uses direct REST instead of an SDK

Volanea exposes a REST send endpoint at POST /v1/send. In a Deno-based Supabase Edge Function, direct fetch is the smallest compatible integration surface: it avoids installing a Node-only email dependency and makes the HTTP request, headers, and error handling explicit.

The only package installed below is the Supabase CLI, which creates, serves, and deploys the Edge Function. The email request itself has no additional runtime dependency. This is intentional, not a missing install step.

For additional request fields, templates, batch sending, and API behavior, consult the email API reference and setup guides before extending the payload.

Prerequisites

Have the following ready before you create the function:

  1. A Supabase project. You need a local project directory connected to Supabase before deployment.
  2. Node.js 20 or later if you install the Supabase CLI through npm.
  3. A Volanea secret API key. Volanea secret keys use an sk_... or test-key format. Treat this value as a password.
  4. A verified Volanea sending domain. The From address in the example must use that domain.
  5. A recipient address you control. Use this for your first delivery test.
  6. A container runtime if you plan to run the complete Supabase stack locally with supabase start.

Keep the sending identity separate from the test recipient. For example, notifications@yourdomain.com can be the sender and you@your-inbox.com can be the recipient. Do not use placeholder addresses such as example.com and expect delivery.

Required environment variables

This guide uses three environment variables:

VariablePurposeExample
VOLANEA_API_KEYSecret key used to authenticate to Volaneask_...
VOLANEA_FROM_EMAILVerified From address used by the messagenotifications@yourdomain.com
VOLANEA_TEST_RECIPIENTInbox that receives the first test messagedeveloper@yourcompany.com

The API key must not be prefixed with SUPABASE_, exposed through a frontend build variable, or committed to a repository. A function secret is the appropriate scope because it is needed only by server-side code.

Install the Supabase CLI dependency

Supabase Edge Functions are TypeScript files executed by the Supabase Edge Runtime. You do not need npm install inside the function directory to use fetch; Deno provides it globally.

Install the Supabase CLI as a development dependency in your application repository:

npm install supabase --save-dev

Because this is a project-scoped installation, run CLI commands through npx supabase rather than assuming a globally installed supabase command. Verify the installation:

npx supabase --help

Initialize Supabase in the repository if it has not already been initialized:

npx supabase init

Create a new Edge Function named send-volanea-email:

npx supabase functions new send-volanea-email

That command creates a function directory similar to this:

supabase/
  functions/
    send-volanea-email/
      index.ts

Replace the generated index.ts contents with the complete handler in the next section.

Complete Supabase Edge Function code

Create or replace supabase/functions/send-volanea-email/index.ts with this code:

import "jsr:@supabase/functions-js/edge-runtime.d.ts";

const VOLANEA_API_KEY = Deno.env.get("VOLANEA_API_KEY");
const VOLANEA_FROM_EMAIL = Deno.env.get("VOLANEA_FROM_EMAIL");
const VOLANEA_TEST_RECIPIENT = Deno.env.get("VOLANEA_TEST_RECIPIENT");

function jsonResponse(body: unknown, status = 200): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: {
      "Content-Type": "application/json",
    },
  });
}

Deno.serve(async (request: Request): Promise<Response> => {
  if (request.method !== "POST") {
    return jsonResponse(
      { error: "Method not allowed. Send a POST request." },
      405,
    );
  }

  if (!VOLANEA_API_KEY || !VOLANEA_FROM_EMAIL || !VOLANEA_TEST_RECIPIENT) {
    console.error("Missing one or more Volanea environment variables.");

    return jsonResponse(
      {
        error:
          "Server configuration is incomplete. Set VOLANEA_API_KEY, VOLANEA_FROM_EMAIL, and VOLANEA_TEST_RECIPIENT.",
      },
      500,
    );
  }

  const idempotencyKey = crypto.randomUUID();

  try {
    const volaneaResponse = await fetch("https://api.volanea.com/v1/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${VOLANEA_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify({
        from: VOLANEA_FROM_EMAIL,
        to: [VOLANEA_TEST_RECIPIENT],
        subject: "Welcome from Supabase Edge Functions",
        html: `
          <h1>Your Volanea integration is working</h1>
          <p>This transactional email was sent by a Supabase Edge Function.</p>
          <p>You can now replace this test content with an application event.</p>
        `,
        text: "Your Volanea integration is working. This transactional email was sent by a Supabase Edge Function.",
      }),
    });

    const responseText = await volaneaResponse.text();
    let responseBody: unknown = responseText;

    try {
      responseBody = JSON.parse(responseText);
    } catch {
      // Preserve a non-JSON upstream response as text.
    }

    if (!volaneaResponse.ok) {
      console.error("Volanea send request failed", {
        status: volaneaResponse.status,
        responseBody,
      });

      return jsonResponse(
        {
          error: "Volanea rejected the send request.",
          details: responseBody,
        },
        volaneaResponse.status,
      );
    }

    return jsonResponse({
      message: "Transactional email accepted by Volanea.",
      idempotencyKey,
      volanea: responseBody,
    });
  } catch (error) {
    console.error("Unexpected error while calling Volanea", error);

    return jsonResponse(
      {
        error: "Unable to reach Volanea from this Edge Function.",
      },
      502,
    );
  }
});

This is a complete Deno Edge Function, not pseudocode. It reads secrets once when the function instance initializes, validates them before sending, awaits the outbound HTTP request, and returns JSON for both successful and unsuccessful provider responses.

The request body sends one message with a From address, one recipient array, subject, HTML body, and text fallback. Including both html and text is a sound transactional-email default: clients that do not render HTML can display the plain-text message, and recipients can still understand the notification when HTML is disabled.

What each part of the function does

The first import supplies Supabase Edge Runtime types. It does not add an email SDK or produce a client-side bundle.

Deno.env.get() reads runtime secrets. It returns string | undefined, which is why the handler validates all three values before calling the API. A missing API key should produce an internal configuration error, not an outbound request with Bearer undefined.

Deno.serve() starts the HTTP handler. The handler accepts only POST so a browser visit or uptime monitor cannot trigger an email with a simple GET request.

crypto.randomUUID() creates one idempotency value for this send attempt. Volanea supports the Idempotency-Key header for safe retries. If your network fails after the provider receives the request, retrying with the same key helps prevent a duplicate email. For this introductory endpoint, the key is generated per invocation. In a production flow, derive or persist it from a durable event identifier such as welcome:user_123 or receipt:order_456.

The function reads the upstream body with .text() first, then attempts to parse JSON. This prevents an exception if an intermediary or upstream service sends a non-JSON error page. It also returns the provider’s status code rather than incorrectly returning 200 when the send request failed.

Configure local secrets

For local development, create a file named .env.local inside the supabase directory:

VOLANEA_API_KEY=sk_replace_with_your_secret_key
VOLANEA_FROM_EMAIL=notifications@your-verified-domain.com
VOLANEA_TEST_RECIPIENT=you@your-inbox.com

Do not commit this file. Add it to .gitignore if it is not ignored already:

supabase/.env.local

Use your actual verified sender address. The VOLANEA_FROM_EMAIL value should be an email address, not a display-name string and not an unverified domain. If you need a display name or Reply-To setting later, add it only after confirming the request fields in the Volanea API reference.

You can start the local Supabase services if your environment is configured for local development:

npx supabase start

Then serve the Edge Function with the local environment file:

npx supabase functions serve send-volanea-email --no-verify-jwt --env-file supabase/.env.local

The --no-verify-jwt flag is suitable for a controlled local test because it lets you invoke the function without configuring a user token. Do not carry that flag into a production deployment just because it made local testing easier. Production access should match your application’s authorization model.

Test the function locally

With the local function server running, invoke it with a POST request:

curl -i --request POST \
  "http://localhost:54321/functions/v1/send-volanea-email" \
  --header "Content-Type: application/json" \
  --data '{}'

The function does not need request data for this first test because the sender and recipient are configured through environment variables. A successful response should contain Transactional email accepted by Volanea. along with the API response returned by Volanea.

Check the test inbox, including its spam or junk folder during early domain setup. Also inspect the terminal running supabase functions serve; the function logs configuration errors, transport exceptions, and non-success responses from Volanea without printing the API key.

Deploy the function and set production secrets

Local .env.local values do not automatically become hosted Supabase secrets. Set each variable in the Supabase project before deployment:

npx supabase secrets set VOLANEA_API_KEY=sk_replace_with_your_secret_key
npx supabase secrets set VOLANEA_FROM_EMAIL=notifications@your-verified-domain.com
npx supabase secrets set VOLANEA_TEST_RECIPIENT=you@your-inbox.com

Deploy the function:

npx supabase functions deploy send-volanea-email

After deployment, invoke the function from a trusted server-side workflow or from your application through Supabase Functions. Keep the first deployed test recipient fixed until you have added application-specific authorization and recipient validation.

A secure production design usually does not leave a generic public “send email” endpoint available to every browser visitor. Instead, authenticate the caller, derive the recipient and message context from trusted database records, and allow only the exact event your application supports. For example, a receipt function should accept an orderId, verify that the signed-in user owns that order, load the order from the database, and send the receipt to the stored billing email.

Choose an authorization model before accepting user input

The sample intentionally ignores the request body. That may feel restrictive, but it avoids several common abuse paths: attackers choosing recipients, injecting unexpected HTML, sending a high volume of mail, or using your endpoint as an open relay.

When you make the endpoint dynamic, validate every value at the server boundary:

  1. Require an authenticated caller unless the action is explicitly designed for unauthenticated use.
  2. Resolve the recipient from a trusted record when possible instead of accepting an email address from the browser.
  3. Validate any input that must be accepted, including IDs, locale values, and template variables.
  4. Limit which template or message type can be requested.
  5. Record a stable idempotency key for the business event.
  6. Apply rate limits to actions that could be automated, such as resend-verification or password-reset requests.

The same principles apply if a database webhook, scheduled job, or another backend service invokes the function. The trigger mechanism changes, but the Volanea key and message-construction rules remain server-side.

Make the email production-ready

A successful API response means the provider accepted your request. It does not mean that a recipient opened the email or that every mailbox provider will display it identically. Transactional delivery is a system that includes domain authentication, payload quality, retry behavior, event processing, and observability.

Use a verified, consistent sender

Use a From address on a domain you control and have verified. Avoid rotating between unrelated domains or sending identities. Consistency helps recipients recognize your product and makes support investigations easier when someone reports a missing email.

Keep different categories of email logically separated. For example, account-security mail, product notifications, and marketing broadcasts may need different sender identities, consent rules, and operational monitoring. Do not mix promotional copy into a password reset or a receipt just because the messages share the same API.

Include both HTML and text content

The sample includes a simple HTML body and a text alternative. In your real message, keep the two versions semantically equivalent. The text version should not be an afterthought; it is useful for plain-text clients, accessibility workflows, security-conscious readers, and troubleshooting.

For HTML email, prefer simple, portable markup. Email clients differ substantially in CSS support, and modern browser layout features do not always translate well to inboxes. Use clear headings, short paragraphs, absolute HTTPS links, visible call-to-action text, and a readable fallback path for users who cannot click a button.

Treat idempotency as a business requirement

Network failures are ambiguous. Your function may time out after Volanea has already accepted a message, or the client may retry before it receives the response. If each retry generates a new email request, the recipient can get duplicate receipts, duplicate invitations, or duplicate alerts.

For one-time events, create a deterministic idempotency key based on the event that should happen exactly once. Examples include invoice-paid:in_123, workspace-invite:inv_456, or password-reset:request_789. Store the event state in your database where appropriate, and reuse the same key only for retries of that exact event.

Do not use one static key for all messages. A static value would cause distinct sends to be treated as duplicates. The key must identify one logical delivery action, not the entire application.

Log identifiers, not secrets or entire email bodies

Log enough context to investigate delivery without creating a privacy problem. Useful log fields include a request ID, your internal user or order ID, a message type, an idempotency key, the upstream HTTP status, and the provider response identifier when available.

Avoid logging your Volanea API key, session tokens, full customer profiles, password-reset URLs, or the complete content of confidential emails. Logs are often accessible to more people and retained longer than application data. Redact sensitive values before reporting errors to monitoring tools.

Common errors

Most Supabase and email-integration failures are predictable. Use the status code, function logs, and request construction to identify which boundary failed: the client-to-function request, function configuration, or function-to-Volanea API request.

401 or 403 authentication failures

An authentication error from Volanea usually means the Authorization header is missing, malformed, or uses the wrong credential. Confirm that the function sends:

Authorization: `Bearer ${VOLANEA_API_KEY}`

Also confirm that VOLANEA_API_KEY exists in the environment where the function is running. A value in supabase/.env.local is available only when you pass that file to local serving; it is not automatically present in the deployed function. Set hosted secrets with npx supabase secrets set ... and redeploy or invoke the hosted function again.

Do not place the key in browser code, a VITE_ variable, a NEXT_PUBLIC_ variable, or a mobile application. Those prefixes and environments are designed for values users can inspect.

400 or 422 validation failures

A request validation failure usually indicates a malformed message payload. Check the sender, recipient, subject, and content fields first. The sender must be a valid address on a configured sending domain, recipients must be valid email addresses, and the body must be valid JSON.

Set the JSON media type exactly:

"Content-Type": "application/json"

Without it, the provider may not parse the body as JSON. Do not use multipart/form-data, a form-encoded body, or a JavaScript object directly as body; fetch requires the serialized string created by JSON.stringify(...) for this request.

The function sends no email because await is missing

fetch() is asynchronous. If you call it without await and return the HTTP response immediately, the Edge Function may finish before the outbound request completes or you may lose the provider error information.

Correct:

const volaneaResponse = await fetch("https://api.volanea.com/v1/send", options);

Incorrect:

fetch("https://api.volanea.com/v1/send", options);
return new Response("sent");

Always await the send request and inspect volaneaResponse.ok. A network request resolving does not automatically mean the provider accepted the message; HTTP error responses still resolve to a Response object.

Unexpected token or JSON parsing errors

Do not assume every upstream response is JSON. Error pages from a proxy, a temporary platform issue, or an unexpected service response can be plain text or HTML. The sample reads .text() first, then attempts JSON.parse() inside a nested try block.

Similarly, do not call both response.json() and response.text() on the same response. A response body can be consumed only once. Pick one strategy, or read text once and parse that string yourself as the sample does.

405 Method Not Allowed

The sample handles only POST. Use curl --request POST, call the function with a POST request from your application, or change the handler intentionally if your workflow needs another method. Do not change the handler to send on GET merely to make testing in a browser convenient; GET requests can be triggered by prefetchers, crawlers, bookmarks, and monitoring systems.

CORS failures from a browser

A direct browser invocation may trigger a CORS preflight request using OPTIONS. The first sample does not include permissive CORS headers because its safest use is a server-side invocation or an authenticated application call with a narrowly designed endpoint.

If you need browser access, add an explicit OPTIONS handler and restrictive Access-Control-Allow-Origin, Access-Control-Allow-Headers, and Access-Control-Allow-Methods headers for your actual application origin. Do not use wildcard CORS for an endpoint that can send email unless the endpoint has robust authorization, input validation, and abuse controls.

The request succeeds but the email is not in the inbox

First, distinguish provider acceptance from final recipient delivery. Check the recipient’s spam and junk folders, verify the From domain configuration, and inspect Volanea delivery events or message activity. A test recipient may also have a mailbox rule, a full inbox, a suppressed address, or an organization-level filter.

Use a real verified domain and a recipient address you control for initial testing. Then test across at least two mailbox providers before treating an email design or sending domain as production-ready.

Next steps: templates, webhooks, and application events

Once the one-message test works, move the message content out of the function where that improves maintainability. Templates let you reuse a controlled email design while passing event-specific data such as a customer name, order number, workspace name, or action URL. The function should choose an approved template and provide validated data rather than accept arbitrary HTML from a client.

Add webhooks when your application needs to react after the send request. Delivery, bounce, complaint, and engagement events can update your own records, stop follow-up actions, surface support context, or improve operational monitoring. Webhook handlers should verify signatures, return quickly, tolerate retries, and be idempotent just like outbound sends.

For event-driven application flows, consider separating “decide that an email should be sent” from “send the email now.” Store an event or outbox record in your database, process it in a trusted worker or function, and record the Volanea response. That design gives you a durable audit trail, clearer retry behavior, and less risk that a transient request failure loses an important customer notification.

FAQ

Do I need a Volanea SDK to send email from a Supabase Edge Function?

No. Supabase Edge Functions run on Deno and provide fetch globally, so the function can call Volanea’s REST API directly. Install the Supabase CLI for local development and deployment, then use fetch for the email request.

Where should I store the Volanea API key?

Store it in Supabase Edge Function secrets and read it with Deno.env.get("VOLANEA_API_KEY"). For local development, keep it in an uncommitted environment file such as supabase/.env.local. Never expose it to a browser or commit it to Git.

Can I send to an address supplied by the frontend?

You can, but it is safer to resolve recipients from trusted server-side data. If user input is necessary, authenticate the caller, validate the address and the business action, rate-limit the endpoint, and do not allow arbitrary HTML or sender identities.

Why should I send both HTML and text?

HTML provides a branded, structured email experience, while text provides a readable fallback for clients or users that do not render HTML. Sending both also makes operational testing and accessibility reviews easier.

How do I prevent duplicate transactional emails?

Use an Idempotency-Key tied to one durable business event, such as an order ID or invitation ID, and reuse that same key only when retrying the same send. Do not generate a new key for every retry of a message that should be delivered once.