Supabase Edge Functions run TypeScript on the edge — Deno-based, deployed via the Supabase CLI, billed per invocation. They’re the right tool for jobs that need to live outside your Next.js app: webhook receivers, scheduled tasks, cross-tenant utilities that shouldn’t be per-site-deploy. They’re the wrong tool for the things most teams reach for them on (replacing API routes, server actions, or per-tenant business logic). Here’s the decision framework we use.
What an Edge Function actually is
A single TypeScript file deployed to the Supabase project. It runs on Cloudflare-style edge infrastructure, has access to the Supabase service-role key and your environment variables, can be invoked via HTTP, by a cron schedule, or by a database trigger. Cold starts are ~50ms; warm invocations are sub-10ms.
// supabase/functions/score-lead/index.ts
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
serve(async (req) => {
const { lead } = await req.json();
const score = computeScore(lead); // your scoring logic
return new Response(JSON.stringify({ score }), {
headers: { "content-type": "application/json" },
});
});
function computeScore(lead: unknown): number {
// …
return 0;
}Three cases where Edge Functions earn their keep
- Webhook receivers from third-party services (Stripe, Twilio, Resend). The function lives at a stable URL across all clients, doesn’t need a per-site deploy to update, and can write to any tenant table.
- Scheduled jobs. Daily lead-score refresh, weekly digest emails, hourly sitemap regeneration. Supabase has a built-in scheduler — set a cron expression, the function runs.
- Cross-tenant utilities. A function that processes data across all clients’ submissions tables. Easier to maintain in one place than in N per-site Next apps.
Three cases where a Next.js API route or server action is better
- Per-tenant form submissions. Use a server action in the tenant’s Next app — closer to the user, ships with the deploy, no extra layer.
- Anything that needs Node-only APIs. Edge Functions are Deno-based; not every npm package works there. A Next.js API route gives you the full Node ecosystem.
- Anything that needs to read request cookies for an authenticated session. Edge Functions don’t share cookies with your Next app — you’d have to pass tokens manually.
Pattern: the lead-score function
FH’s lead-scoring lives in an Edge Function because the same scoring rubric applies to every tenant’s submissions. The function reads a submission by ID, computes the score using the 100-point rubric (intent 30 / contact 25 / phone 20 / message 15 / method 10), and writes the score back to the row. The function is invoked by a database trigger on every INSERT into `submissions`.
// supabase/functions/score-lead/index.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { serve } from "https://deno.land/std@0.224.0/http/server.ts";
const admin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
serve(async (req) => {
const { id } = await req.json();
const { data: lead } = await admin
.from("submissions")
.select("*")
.eq("id", id)
.single();
if (!lead) return new Response("not found", { status: 404 });
const score = scoreLead(lead);
await admin
.from("submissions")
.update({ score })
.eq("id", id);
return new Response(JSON.stringify({ score }));
});
function scoreLead(lead: any): number {
let s = 0;
if (lead.message?.length > 80) s += 15;
if (lead.phone) s += 20;
// …
return s;
}Webhook receivers: the Resend example
We use Resend to send transactional email across the client book. Resend posts delivery/open/click events to a webhook. The webhook lives in an Edge Function, not in any of the client sites — one stable URL, written once, updates without a redeploy.
Scheduling: pg_cron + Edge Functions
Supabase ships pg_cron. Schedule a SQL job that calls an Edge Function via `net.http_post()`. Cron expression in Postgres, function logic in TypeScript, no separate scheduler infrastructure.
select cron.schedule(
'daily-sitemap-refresh',
'15 3 * * *', -- 3:15 AM UTC every day
$$
select net.http_post(
url := 'https://your-project.supabase.co/functions/v1/refresh-sitemaps',
headers := jsonb_build_object('Authorization', 'Bearer ' || current_setting('app.functions_key'))
);
$$
);Logs and debugging
Edge Function logs surface in the Supabase dashboard. They’re searchable, structured, and retain for 7 days. For long-term observability, pipe them to a separate destination (Better Stack, Axiom). We use Better Stack because the alerting is good and the cost is flat at our volume.
Local development
`supabase functions serve` runs the function locally with hot-reload. Pair it with the local Supabase stack (`supabase start`) and you can develop the full flow without touching production. We test every function locally before deploying — Deno is permissive but the deno runtime has more subtle differences from Node than people expect.
Cost: free until you’re really using it
Supabase Pro includes 500k function invocations per month. Across the FH client book we run about 80k per month — score-lead on every submission, four cron jobs, two webhook receivers. The free included tier covers us 6× over. Overage is $2 per million invocations.
When NOT to use Edge Functions
- When the logic is tightly coupled to a single tenant. Ship it with that tenant’s Next.js app instead.
- When you need Node-only npm packages.
- When you need to read request cookies from your Next app (the auth context doesn’t carry over).
- When latency requirements are sub-50ms and you can’t tolerate the cold start.
- When you need stable IP addresses for IP-allowlisting on a third-party service. Edge IPs rotate; run an Express app behind a static IP instead.
How this lands across FH client work
Across the client book we run six Edge Functions: lead scoring, Resend webhook receiver, Twilio Lookup proxy (used before Google Ads lead-form integration), a daily sitemap refresh, a weekly digest email job, and a slop-detector trigger that runs on new uploads to image buckets. Six functions for the entire client book. Everything else lives in the per-site Next apps where it belongs.
If you’re reaching for Edge Functions to replace every API route, you’re using the wrong tool. If you’re ignoring them entirely, you’re duplicating per-tenant logic across every deploy. Book a consultation if you want a second opinion on which side of the line your work lives on.