Skip to content
Supabase+Edge Functions
Stack Integration

Supabase Edge Functions: Server-Side Logic Guide

Supabase Edge Functions run Deno-based TypeScript at the edge — the right place for webhook handlers, third-party API calls, and any server-side logic that shouldn't touch your client.

Use Cases
  1. Stripe webhook handler verifying signatures and updating Postgres
  2. Transactional email dispatch triggered by Postgres database events
  3. Third-party API calls (Resend, Twilio, OpenAI) keeping secret keys server-side
  4. Custom auth hooks running business logic on sign-up or sign-in
Implementation

Deploy with supabase functions deploy <function-name>. Edge Functions run on Deno — use import syntax and URL imports for dependencies. Access Supabase Postgres from within an Edge Function using the service role key stored in the function's environment variables (never expose it to clients). For Postgres-triggered functions, wire a database webhook in the Supabase dashboard to call your Edge Function URL on insert/update/delete events.

In detail

How the pieces connect

An Edge Function is a Deno runtime that boots per request, separate from your Postgres database and from your client app. Three actors talk to each other: the browser (using the anon key under Row Level Security), the function (using the service role key, which bypasses RLS), and Postgres. The function reaches the database with a second createClient call configured with SUPABASE_SERVICE_ROLE_KEY, so writes that the anon client can't make — flipping an order to paid, inserting an audit row — happen server-side. Database webhooks close the loop the other way: an INSERT, UPDATE, or DELETE on a table fires an HTTP POST to the function URL with the changed row in the body, letting Postgres push work out instead of the client polling. Auth Hooks plug into the sign-up and sign-in path itself rather than reacting after the fact.

Production gotchas

The Stripe webhook is where most teams trip. You must verify the signature with stripe.webhooks.constructEventAsync — the async variant, because the Deno runtime uses Web Crypto, not Node's sync crypto. Construct the event off the raw request body (await req.text()), since any JSON re-serialization breaks the HMAC. Read the body once; calling req.text() after req.json() throws. Make handlers idempotent: Stripe retries on non-2xx and can deliver an event more than once, so key on event.id or the Checkout session id and upsert. Cold starts add latency on the first hit after idle — keep payloads small and dependencies few. Secrets are managed with supabase secrets set, read via Deno.env.get; the platform injects SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY automatically, so don't redefine them.

Security considerations

The service role key is the whole ballgame: it bypasses every RLS policy, so it lives only in function env vars and never reaches a browser bundle or a client createClient. If a function acts on behalf of a signed-in user, forward their JWT in the Authorization header and create a per-request client with that token so RLS still applies to their data — reserve the service role client for the few operations that genuinely need elevation. By default a deployed function expects a valid JWT; for public webhook endpoints (Stripe, Twilio) you deploy with --no-verify-jwt and replace that gate with the provider's own signature check, otherwise the endpoint is open. Validate and narrow CORS headers rather than echoing * for any route a browser calls directly.

When this combo fits — and when it doesn't

Edge Functions fit when logic must stay server-side: verifying a webhook signature, calling Resend or OpenAI with a secret key, or running privileged writes that RLS would otherwise block. They suit event-driven, short-lived work triggered by HTTP or a database webhook. They fit less well for long-running jobs, heavy CPU work, or anything needing a persistent connection — the request-scoped model and execution limits work against you, and a queue or dedicated worker is the better tool. If a rule can be expressed as an RLS policy or a Postgres trigger, keep it in the database instead of a function round-trip. On Pizzeria Bestek (React + Supabase), the pattern that earned its keep was exactly this: order and payment logic handled server-side, with the client left holding only the anon key.

Live ExamplePizzeria Bestek — Case Study
Other integration guidesView all →
Related

Need this built?