Skip to content
Vibe Coders

Supabase Edge Functions: When to Use Them, When to Skip

Supabase Edge Functions run on Deno, not Node, and that changes everything. Here is when they beat Vercel API routes, when they cost you a week, and how the hybrid pattern actually works.

By WitsCode10 min read

You wired up a Supabase project over the weekend, the docs pointed you at Edge Functions for anything server-side, and now you are three days into a migration that should have taken three hours. Half your npm packages refuse to import, the Stripe SDK works but Puppeteer does not, and your team is asking whether any of this should live in Vercel instead. This is the conversation nobody has at the start of a build and everybody has two weeks in.

The short version is that Supabase Edge Functions are a fantastic tool for a narrow set of jobs and a terrible tool for the jobs next to them. They run on Deno, not Node, which changes what you can import. They run inside the Supabase data plane, which changes what you should run there. Vercel API routes run on Node, have the full npm ecosystem, and sit next to your frontend code. Neither wins outright. What wins is putting each endpoint in the runtime that matches its job, and the criteria are not obvious from the marketing pages.

What Supabase Edge Functions actually are

Edge Functions are TypeScript handlers that run on Deno Deploy, a globally distributed isolate runtime that Supabase resells as part of its platform. The handler signature is a plain Deno.serve((req: Request) => Response) that takes a standard fetch Request and returns a standard Response. There is no Express-style req and res, no middleware chain, no body parser you wire up. The web platform primitives do the work and you write against them directly.

The runtime is Deno, which matters because Deno is not Node. Deno has its own standard library under the node: and std: specifiers, it resolves npm packages through an npm: prefix, and it runs in a V8 isolate rather than a traditional process. Most pure-JavaScript and pure-TypeScript npm packages import cleanly. The Supabase client, Stripe SDK, Resend SDK, OpenAI SDK, Zod, date-fns, nanoid, all work. Anything with a native addon, a postinstall script that downloads a binary, or a dependency on Node's child_process does not. Sharp does not work. Puppeteer does not work. node-canvas does not work. Most ORMs that compile bindings at install time do not work. Prisma technically runs via the data proxy but the setup is painful enough that it is rarely worth it on Edge Functions.

The second thing that matters is where the isolates live. Edge Functions run in the same regional infrastructure as your Supabase Postgres instance, which means a database call from a function goes over the internal network rather than out to the public internet and back. For an endpoint that runs three queries per request, that is fifty to two hundred milliseconds you do not pay. A Vercel function calling the same database has to reach out across the internet every time. This is the single most under-sold feature of Edge Functions and the reason they exist at all.

The third thing to know is the limits. Edge Functions cap out at four hundred seconds of wall clock on paid plans and one hundred and fifty on free, with a hard CPU time limit much tighter than that. Memory is two hundred and fifty-six megabytes. Outbound fetch calls are capped at one thousand per invocation. These numbers are plenty for webhooks and auth hooks. They are ruinous for anything that needs to process a large file, render a PDF with custom fonts, or run even modest image manipulation.

When Edge Functions are the right tool

The clearest win case is auth hooks. Supabase lets you hook into the auth pipeline at several points: Send SMS, Send Email, Custom Access Token, MFA verification, Password Verification. These run synchronously during a login or signup flow, which means they have to return in well under a second or your user stares at a spinner. They also need to query your database to look up org membership, feature flags, or plan claims to stitch into the token. Because Edge Functions run next to the database, each of those queries costs single-digit milliseconds instead of the eighty to two hundred a Vercel round trip would add. Put the Custom Access Token hook in an Edge Function and token generation stays snappy even when you are enriching claims from three tables.

The second clear win is inbound webhooks that write to your database. Stripe webhooks, Clerk webhooks, GitHub webhooks, Twilio status callbacks. The pattern is always the same: verify a signature, parse a JSON body, write some rows, return two hundred. None of that needs Node-specific libraries, all of it benefits from being close to the database, and Stripe in particular will start retrying aggressively if your acknowledgement takes longer than a few hundred milliseconds. A Stripe webhook handler in an Edge Function consistently acks in under two hundred milliseconds once warm. The same handler on Vercel routinely takes four to eight hundred once you count cold start plus database round trip.

The third win is internal endpoints that read heavily from Supabase. Search autocomplete, presence, notification feeds, anything where the request fans out into three or four queries against the same Postgres. The in-data-plane advantage compounds with every query. Put the same endpoint on Vercel and you pay the network tax four times per request.

The fourth is scheduled jobs. Pair pg_cron with pg_net to invoke an Edge Function on a cron expression stored in the database itself. This is the idiomatic Supabase pattern for scheduled work and it keeps everything inside one platform with one bill, one log stream, and one set of secrets. The alternative, a Vercel Cron calling back into Supabase, works but you are paying the network tax plus managing schedules in two places.

The fifth is database trigger side effects. A row-level trigger fires pg_net, which calls an Edge Function, which calls Stripe or Resend or OpenAI. The trigger runs in the database, the function handles the outbound IO, and the boundary is clean. Trying to wire this up to Vercel works but adds latency at every hop and makes failure modes noisier.

The npm compatibility wall and how to read it

The number one reason migrations stall is a package that refuses to import. The rule to remember is straightforward: pure JavaScript and TypeScript packages on npm generally work through the npm: specifier, and anything with a native addon or a postinstall that downloads or compiles binaries probably does not. import { Resend } from "npm:resend@3" works because Resend is a thin fetch wrapper. import sharp from "npm:sharp" breaks because sharp ships a compiled binary for every platform and Deno Deploy cannot load it.

The edge cases bite hardest. The Node standard library is partially emulated: node:crypto, node:buffer, node:stream, node:url, and node:path all work. node:fs works for read-only access to files bundled with your function. node:child_process does not work at all. Anything that opens a raw socket outside of fetch does not work. Packages that depend indirectly on any of these blow up at runtime rather than import time, which is the worst kind of error to discover after deploy.

The practical check before you commit to putting a library inside an Edge Function is to grep its package.json for a binary field, a gypfile flag, or a postinstall script that references node-gyp, prebuild, or a download URL. If you see any of those, assume it will not work and pick a different package or a different runtime.

When to skip and use Vercel instead

Anything that needs a Node-specific package belongs on Vercel. If your endpoint renders a PDF with custom fonts using PDFKit, processes images with sharp, drives Chromium through Playwright, or uses a SaaS SDK that has not done the work to be Deno-compatible, do not fight the runtime. Put it on Vercel as an API route or a Server Action and move on. The week you spend trying to find a Deno-compatible alternative is almost always more expensive than the network latency you were trying to save.

Long-running or CPU-heavy work also belongs off Edge Functions. Video transcoding, AI model inference that takes more than a few seconds, ZIP generation over large archives, anything that buffers more than two hundred and fifty megabytes in memory. Vercel Functions under Fluid Compute go up to eight hundred seconds and three gigabytes of memory, which is enough for most of these jobs. Past that, you want Fly Machines, Railway, or an AWS Lambda container image.

Streaming AI responses are a nuanced case. The Deno runtime can stream fine, and the Vercel ai SDK has Deno support. But if your OpenAI or Anthropic call dominates the latency budget, being close to your database does not help you, and the Next.js React Server Components integration is a first-class experience on Vercel that you lose if you move the endpoint. For most vibe-coded AI apps the streaming endpoint stays in Vercel and the Supabase writes happen from there, or asynchronously through a webhook to an Edge Function after the stream completes.

Frontend-adjacent API routes that do not touch Supabase should stay on Vercel. If an endpoint serves your Next.js app, reads from a third-party API, and returns a response, putting it in a Supabase Edge Function adds a hop, a separate auth boundary, and a CORS round trip for no benefit. The close-to-the-database argument does not apply when the database is not involved.

Complex monorepos with shared TypeScript between frontend and backend also push you toward Vercel. Supabase Edge Functions are a separate project tree with their own tsconfig-equivalent, typically a deno.json or an import map, and they cannot pull in workspace packages without a pre-bundle step. The first time you try to share a Zod schema between a page component and an Edge Function you hit this wall. Some teams solve it by generating the schema from the database and importing it on both sides. Most teams just put the endpoint on Vercel and keep the shared imports clean.

Cold starts, execution time, and the quieter limits

Deno Deploy isolates cold-boot faster than Node lambdas, typically fifty to two hundred milliseconds against the three hundred to fifteen hundred you would see on a cold Node function. But cold is still cold. An isolate that has not served a request for roughly five minutes will be evicted and the next request pays the boot cost. For low-traffic endpoints this matters. For high-traffic endpoints it does not because you are always warm. Do not promise your team sub-fifty-millisecond p99 on a Supabase Edge Function. Promise warm-request p99 and be honest about the cold cases.

Execution time and memory caps are the quieter gotcha. A handler that processes a ten-megabyte JSON body and does three database writes is fine. A handler that assembles a two-hundred-megabyte PDF from a template and a set of images will get killed at the memory ceiling before it finishes. Outbound fetch is capped at one thousand calls per invocation, which sounds infinite until you are batching a sync against a paginated API with fifty calls per page and ten thousand pages.

CPU time is stricter than wall time. A function that sits waiting on an external API for two minutes is fine. A function that runs a synchronous loop for ten seconds will often get killed long before the four-hundred-second wall clock matters. Plan around this by pushing heavy transforms out of the hot path and keeping Edge Function handlers mostly IO-bound.

The hybrid pattern experienced teams actually ship

Everyone arrives at the same split eventually. Vercel hosts the Next.js app and its API routes. These handle every user-facing endpoint, every streaming AI response, and anything that needs the Node ecosystem or shared TypeScript with the frontend. Supabase Edge Functions handle auth hooks, inbound webhooks, pg_cron scheduled jobs, and internal endpoints whose performance is dominated by database latency. A third tier, usually Fly Machines or an AWS Lambda container, handles CPU-heavy jobs that do not fit in either runtime.

All three call the same Postgres. The service role key lives only in server-side environment variables on each side, never in a client bundle, never committed. Observability splits across the three providers, which is the one real cost of the hybrid pattern, and it is the reason solo founders sometimes keep everything on Vercel for the first six months and only reach for Edge Functions once they have a real webhook or a real auth hook to justify the split.

If you only remember one thing, make it this. The decision is not Supabase versus Vercel. It is per endpoint. An auth hook belongs on Supabase, a Stripe webhook belongs on Supabase, a streaming LLM endpoint belongs on Vercel, a PDF generator belongs off both. Build the map once for your app and you stop guessing.

Where WitsCode fits → runtime selection advisory

Most vibe-coded apps we audit have the runtime map backwards. Everything lives in Next.js API routes when four endpoints should be Supabase Edge Functions, or the whole API lives in Edge Functions because someone followed the Supabase quickstart and then the team hit the npm compatibility wall and froze mid-migration. We look at every server-side endpoint in your app, sort them by the rules in this guide, and give you a migration plan that moves each one into the runtime it belongs in without breaking auth, webhooks, or types. If you are already on the wrong side of a half-finished migration, we can finish it this week. Book a runtime audit →

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

MVP Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss vibe coders for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.