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:
- 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.
- 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?
- Native TypeScript support: No tsconfig needed, no ts-node to install. Just write
.tsfiles and run them. For those of us who’ve been writing backends in TypeScript, this saves a lot of configuration time. - 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: 'demo@example.com' } })
})
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:
jsr:@hono/honois Deno’s JSR package format, not npm. JSR is Deno’s official package registry.basePath('/api')sets your route prefix to/api.cis Hono’s context object, containing the request, response, and various utility methods.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:
- Reduce dependency size: Use Deno/JSR native packages when possible, minimize npm packages
- Lazy loading: Load large modules on-demand with
import() - 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
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
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
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
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
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
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?
Are Edge Functions suitable for handling Webhooks?
How is Deno package management different from Node.js?
How do I connect to Supabase database in Edge Functions?
Do Edge Functions have execution time limits?
How do I debug Edge Functions?
10 min read · Published on: Apr 19, 2026 · Modified on: Apr 19, 2026
Supabase in Practice
If you landed here from search, the fastest way to build context is to jump to the previous or next post in this same series.
Previous
Supabase Storage in Practice: File Uploads, Access Control, and CDN Acceleration
Learn the complete workflow for Supabase Storage—from file uploads to permission configuration and CDN integration, covering RLS policies, user isolation, Smart CDN, and image transformations
Part 4 of 7
Next
Supabase Storage in Practice: File Uploads, CDN, and Access Control
A complete practical guide to Supabase Storage: comparison of three access control modes, TUS chunked uploads, Smart CDN optimization tips, and cost analysis against R2/S3. Includes React code examples and troubleshooting solutions.
Part 6 of 7
Related Posts
Supabase Getting Started: PostgreSQL + Auth + Storage All-in-One Backend
Supabase Getting Started: PostgreSQL + Auth + Storage All-in-One Backend
Supabase Database Design: Tables, Relationships & Row Level Security Guide
Supabase Database Design: Tables, Relationships & Row Level Security Guide
Supabase Auth in Practice: Email Verification, OAuth & Session Management

Comments
Sign in with GitHub to leave a comment