Switch Language
Toggle Theme

Supabase Edge Functions in Practice: Deno Runtime and TypeScript Development Guide

At 3 AM, my phone started buzzing like crazy. Stripe Webhooks were throwing 500 errors in production—customers’ payments went through, but orders weren’t being created.

I dragged myself up and checked the logs. The problem turned out to be with my old serverless function—the cold start took too long, and Stripe timed out before it could respond. Even worse, I still needed to set up a separate API gateway for signature verification and CORS handling…

That night changed everything. I started seriously researching Supabase Edge Functions. Honestly, when I first saw “Deno runtime,” I hesitated—after writing Node.js for years, switching runtimes meant learning a whole new set of APIs. But after diving in, I realized Edge Functions aren’t about “migrating” you over. They’re about giving you a lighter alternative, specifically designed for scenarios that don’t need heavyweight dependencies.

This article shares the pitfalls I encountered and what I learned: Edge Functions architecture principles, the differences between Deno and Node.js, local development and debugging workflows, and practical experience building elegant APIs with the Hono framework.

What Are Edge Functions — Architecture and Technology Choices

Let’s start by clarifying what Edge Functions are, and why Supabase chose Deno instead of Node.js.

Edge Execution, Not Cloud Hosting

Edge Functions are TypeScript functions that run on edge nodes. Unlike traditional Lambda or Vercel Functions, they’re not deployed on servers in a few centralized regions. Instead, they’re distributed across hundreds of edge nodes worldwide.

What does this mean? When a user in Shanghai makes a request, the function might execute on an edge node in Tokyo—latency drops from hundreds of milliseconds to just tens of milliseconds.

But the edge comes with trade-offs—functions can’t be too heavy. Each function runs independently in a V8 isolate with its own memory heap and execution thread. Startup time is in milliseconds, but memory is limited and execution time is constrained. So it’s suited for short-lived operations: Webhook processing, OG image generation, third-party API calls, email sending, and the like.

What it’s not suited for: long-running tasks, libraries that depend heavily on Node.js native modules, or operations that need file system access.

Why Deno?

I dug through Supabase’s GitHub Discussions on this question. Here’s the gist of the official explanation:

  1. Fast startup: Deno packages code in ESZip format, achieving 0-5ms cold starts for functions. Compare that to Node.js Lambda cold starts, typically 100-500ms.
  2. Security model: Deno disables file system and network access by default, requiring explicit permission grants. This matters in multi-tenant edge environments—you don’t want someone else’s function reading your data, right?
  3. Native TypeScript support: No tsconfig needed, no ts-node to install. Just write .ts files and run them. For those of us who’ve been writing backends in TypeScript, this saves a lot of configuration time.
  4. Portability: Deno can be embedded into other applications. Supabase uses their own maintained Deno fork called deno_core, specifically optimized for embedded scenarios.

There are trade-offs. Deno’s ecosystem is smaller than Node.js, and some npm packages don’t work directly. But Deno now supports npm specifiers—you can import { xxx } from 'npm:lodash'—and compatibility has improved significantly.

Architecture at a Glance

Here’s the request flow:

Client → CDN/Edge Gateway → JWT Verification → V8 isolate executes function → Response returned

The key part is that JWT verification—Edge Functions verify the Authorization header by default, ensuring only authorized users can call the function. If you want public access, add the --no-verify-jwt flag when deploying.


Development Environment Setup and CLI Commands Explained

Alright, concepts covered. Let’s get hands-on.

Installing Supabase CLI

I’m on macOS, so I used Homebrew:

brew install supabase/tap/supabase

Linux and Windows have their own installation methods. The official docs explain them clearly, so I won’t repeat here.

After installation, log in:

supabase login

This opens a browser for you to authorize the CLI to access your Supabase account.

Initializing a Project

Run this in your project directory:

supabase init

This creates a supabase/ directory with a config.toml configuration file and a functions/ subdirectory (created automatically if it doesn’t exist).

Creating Your First Edge Function

supabase functions new hello-world

This command creates a hello-world/ directory under supabase/functions/ with an index.ts file that looks like this:

Deno.serve(async (req: Request) => {
  const { name } = await req.json()
  const data = {
    message: `Hello ${name}!`,
  }

  return new Response(JSON.stringify(data), {
    headers: {
      'Content-Type': 'application/json',
      'Connection': 'keep-alive',
    },
  })
})

That’s it. Deno.serve() is Deno’s native API that takes a request handler function. Request and Response are standard Web APIs, just like fetch in the browser.

Local Development Server

Start the local development environment:

supabase functions serve --env-file supabase/.env.local

This starts a local server, defaulting to http://localhost:54321. Your function is accessible at http://localhost:54321/functions/v1/hello-world.

Honestly, I hit a snag the first time—I forgot to start Supabase’s local service stack (including local PostgreSQL) first. The correct approach is:

# Start local Supabase stack first
supabase start

# Then start the functions service
supabase functions serve

Testing Requests

Use curl or HTTPie to send a test request:

curl -i --location --request POST 'http://localhost:54321/functions/v1/hello-world' \
  --header 'Authorization: Bearer <your-anon-key>' \
  --header 'Content-Type: application/json' \
  --data '{"name":"World"}'

Response:

{
  "message": "Hello World!"
}

Success.

Hot reload is enabled by default—save your changes and they take effect immediately, no restart needed. The experience is smooth.

Environment Variables

Don’t write sensitive information in your code. Supabase supports managing environment variables via .env files:

# Create .env file
echo "MY_SECRET=super_secret_value" > supabase/.env.local

# Read it in your function
const mySecret = Deno.env.get('MY_SECRET')

When deploying to production, use the supabase secrets set command:

supabase secrets set MY_SECRET=super_secret_value

Practical: Building RESTful APIs with Hono Framework

The native Deno.serve() works fine, but when your function logic gets complex—requiring routing, middleware, parameter validation—handwriting everything becomes painful.

That’s where Hono comes in.

What Is Hono?

Hono is an ultra-lightweight web framework designed specifically for edge runtimes. It supports Deno, Cloudflare Workers, Bun, and other runtimes. Its routing performance is impressive, and TypeScript support is top-notch.

The official description is “small, simple, and ultrafast”—and my experience confirms this.

Integrating into Edge Functions

First, create a new function:

supabase functions new user-api

Then modify index.ts:

import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
import { logger } from 'jsr:@hono/hono/logger'

const app = new Hono().basePath('/api')

// Middleware
app.use('*', cors())
app.use('*', logger())

// Route definitions
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ user: { id, name: 'Demo User', email: '[email protected]' } })
})

app.post('/users', async (c) => {
  const body = await c.req.json<{ name: string; email: string }>()
  // Connect to Supabase database here
  return c.json({ created: body }, 201)
})

app.put('/users/:id', async (c) => {
  const id = c.req.param('id')
  const body = await c.req.json<{ name?: string; email?: string }>()
  return c.json({ updated: { id, ...body } })
})

app.delete('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ deleted: id })
})

// Start server
Deno.serve(app.fetch)

Key points:

  1. jsr:@hono/hono is Deno’s JSR package format, not npm. JSR is Deno’s official package registry.
  2. basePath('/api') sets your route prefix to /api.
  3. c is Hono’s context object, containing the request, response, and various utility methods.
  4. c.json() automatically sets the Content-Type header and handles null and undefined values.

Connecting to Supabase Database

Hono is just a web framework. To work with the database, you need the Supabase client. Here’s a complete example:

import { Hono } from 'jsr:@hono/hono'
import { createClient } from 'jsr:@supabase/supabase-js@2'

const app = new Hono().basePath('/api')

// Initialize Supabase client
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!

const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    autoRefreshToken: false,
    persistSession: false,
  },
})

// GET /api/users - list
app.get('/users', async (c) => {
  const { data, error } = await supabase
    .from('users')
    .select('id, name, email, created_at')

  if (error) {
    return c.json({ error: error.message }, 500)
  }
  return c.json({ users: data })
})

// POST /api/users - create
app.post('/users', async (c) => {
  const body = await c.req.json<{ name: string; email: string }>()

  const { data, error } = await supabase
    .from('users')
    .insert(body)
    .select()
    .single()

  if (error) {
    return c.json({ error: error.message }, 400)
  }
  return c.json({ user: data }, 201)
})

Deno.serve(app.fetch)

Note that I’m using SUPABASE_SERVICE_ROLE_KEY, which has full database permissions and bypasses RLS. Be careful using this in production.

Error Handling and Validation

Hono doesn’t have a built-in validator, but it works well with Zod:

import { z } from 'npm:zod'
import { zValidator } from 'jsr:@hono/zod-validator'

const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
})

app.post(
  '/users',
  zValidator('json', userSchema),
  async (c) => {
    const validated = c.req.valid('json')
    // validated is now a type-safe object
    return c.json({ received: validated })
  }
)

Validation failures automatically return a 400 error with detailed error information in the response body.


Deployment and Production Best Practices

Local testing passed. Time to go to production.

Deployment Command

supabase functions deploy user-api

The first deployment asks which Supabase project to link. Then it automatically uploads code, builds, and deploys.

After successful deployment, the function URL format is:

https://[PROJECT_ID].supabase.co/functions/v1/user-api

Environment Variables and Secrets

Production environment variables need to be set separately:

supabase secrets set SUPABASE_URL=https://xxx.supabase.co
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=eyJxxx...

These Secrets are encrypted and stored. Functions read them via Deno.env.get() at runtime.

JWT Verification Strategy

As mentioned earlier, Edge Functions verify JWT by default. This means:

  • Only requests with a valid Authorization: Bearer <token> header will pass
  • User information from the token can be parsed from req.headers

If you want a public API (for third-party Webhooks, for instance), add --no-verify-jwt when deploying:

supabase functions deploy user-api --no-verify-jwt

But this means anyone can call your function, so you’ll need to implement your own verification in the code.

Reducing Cold Start Latency

While Deno cold starts are fast, there are still things you can do:

  1. Reduce dependency size: Use Deno/JSR native packages when possible, minimize npm packages
  2. Lazy loading: Load large modules on-demand with import()
  3. Keep functions lightweight: One function, one job. Don’t stuff your entire backend into it

Supabase officially recommends keeping single-function execution time under 2 seconds, with cold start times at 0-5ms. So these tips will help you make responses even faster:

Monitoring and Logging

The Dashboard shows function call logs and error reports. You can also integrate Sentry or other monitoring services.

There’s also an EdgeRuntime.waitUntil() API that lets functions continue executing background tasks after returning a response:

EdgeRuntime.waitUntil(
  fetch('https://analytics.example.com/track', { method: 'POST', body: '...' })
)

return new Response('OK')

This way, the client doesn’t have to wait for background tasks to complete before receiving a response.


Conclusion

After all this, what scenarios are Edge Functions suited for?

Well-suited for:

  • Webhook processing (Stripe, GitHub, Slack)
  • OG image generation
  • AI inference (calling LLM APIs)
  • Email and message notifications
  • Short-lived data processing

Not well-suited for:

  • Long-running tasks (video transcoding, for example)
  • Libraries that depend on heavyweight Node.js native modules
  • Operations that need file system access

If you’re already using Supabase’s database and authentication, Edge Functions is a natural extension—no extra servers to set up, no operations to worry about, just write business logic.

How does it compare to Cloudflare Workers or Vercel Functions? I think each has its strengths. Cloudflare Workers is more mature with a larger ecosystem. Vercel Functions has deeper integration with the Next.js ecosystem. But if you’re already on Supabase, Edge Functions offers the best integration experience—database client, authentication, and storage are all ready to go.

If you want to try it out, start with the official examples: github.com/supabase/supabase/tree/master/examples/edge-functions

Questions? Leave a comment, or head to Supabase Discord for community help.

Complete Supabase Edge Functions Development and Deployment Workflow

Complete operations guide from environment setup to production deployment

⏱️ Estimated time: 45 min

  1. 1

    Step1: Install Supabase CLI and log in

    Install the CLI using Homebrew (macOS):

    ```bash
    brew install supabase/tap/supabase
    supabase login
    ```

    Logging in opens a browser to authorize CLI access to your Supabase account.
  2. 2

    Step2: Initialize project and create function

    Run the initialization command in your project directory, then create your first function:

    ```bash
    supabase init
    supabase functions new hello-world
    ```

    This creates a function template in the `supabase/functions/` directory.
  3. 3

    Step3: Start local development environment

    First start the local Supabase stack (including PostgreSQL), then start the functions service:

    ```bash
    supabase start
    supabase functions serve --env-file supabase/.env.local
    ```

    Local function address: `http://localhost:54321/functions/v1/{function-name}`
  4. 4

    Step4: Build API with Hono framework

    Install Hono and create a RESTful API:

    ```typescript
    import { Hono } from 'jsr:@hono/hono'
    import { cors } from 'jsr:@hono/hono/cors'

    const app = new Hono().basePath('/api')
    app.use('*', cors())
    app.get('/users/:id', (c) => {
    return c.json({ user: { id: c.req.param('id') } })
    })
    Deno.serve(app.fetch)
    ```

    Hono supports routing, middleware, and parameter validation.
  5. 5

    Step5: Configure environment variables and secrets

    Use `.env` files for local development, Secrets for production:

    ```bash
    # Local
    echo "MY_SECRET=value" > supabase/.env.local

    # Production
    supabase secrets set MY_SECRET=value
    ```

    Read in functions via `Deno.env.get('MY_SECRET')`.
  6. 6

    Step6: Deploy to production

    Deploy function and set public access (if needed):

    ```bash
    # Standard deployment (requires JWT verification)
    supabase functions deploy user-api

    # Public API (no JWT verification)
    supabase functions deploy user-api --no-verify-jwt
    ```

    Production URL format: `https://[PROJECT_ID].supabase.co/functions/v1/user-api`

FAQ

What's the difference between Supabase Edge Functions and Cloudflare Workers?
Both are edge computing platforms, but with some differences: (1) Edge Functions integrates deeply with Supabase database, authentication, and storage—ready to use out of the box; (2) Deno runtime vs V8 engine—Edge Functions supports npm specifiers and JSR; (3) Both have millisecond-level cold starts with comparable performance. If you're already using Supabase, Edge Functions offers better integration experience.
Are Edge Functions suitable for handling Webhooks?
Very suitable. Webhook processing is a classic Edge Functions use case: (1) Fast cold starts—Stripe, GitHub, and other Webhooks won't time out; (2) Can directly verify signatures, process business logic, and write to Supabase database; (3) Deploy as public API using --no-verify-jwt.
How is Deno package management different from Node.js?
Deno uses JSR (Deno's official package registry) and npm specifiers: (1) JSR packages imported with `jsr:@hono/hono` format; (2) npm packages imported with `npm:zod` format; (3) No package.json or node_modules needed—dependencies are automatically managed. Most popular libraries are supported.
How do I connect to Supabase database in Edge Functions?
Use the @supabase/supabase-js client: (1) Import `jsr:@supabase/supabase-js@2`; (2) Read SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY from environment variables; (3) When creating the client, set auth.autoRefreshToken: false (not needed in edge environment); (4) SERVICE_ROLE_KEY bypasses RLS—be mindful of permission control in production.
Do Edge Functions have execution time limits?
Yes. Supabase officially recommends keeping single-function execution time under 2 seconds, with cold start times at 0-5ms. Suitable for short-lived operations (Webhooks, AI inference, email sending), not for long-running tasks (video transcoding, big data processing).
How do I debug Edge Functions?
Several ways: (1) Local `supabase functions serve` + hot reload—code changes take effect immediately; (2) `console.log()` output shows in Dashboard logs; (3) Use `supabase functions serve --env-file` to load local environment variables; (4) Send test requests with curl or Postman.

10 min read · Published on: Apr 19, 2026 · Modified on: Apr 19, 2026

Related Posts

Comments

Sign in with GitHub to leave a comment