Mastering Supabase Edge Functions with Next.js
Developer Guide

Mastering Supabase Edge Functions with Next.js

Complete guide to building and deploying Supabase Edge Functions with Next.js. Learn serverless functions, Deno runtime, database triggers, webhooks, scheduled jobs, and real-world use cases.

2026-02-25
35 min read
Mastering Supabase Edge Functions with Next.js

Mastering Supabase Edge Functions with Next.js#

Supabase Edge Functions bring serverless computing to your Next.js applications with global distribution, low latency, and seamless database integration. Built on Deno Deploy, they execute close to your users worldwide while maintaining direct access to your Supabase database.

Why Supabase Edge Functions?#

Performance Benefits:

  • Sub-50ms cold starts globally
  • Execute at the edge, close to users
  • No server management required
  • Automatic scaling

Developer Experience:

  • TypeScript/JavaScript support via Deno
  • Direct Supabase client access
  • Local development and testing
  • Simple deployment workflow

Use Cases:

  • Webhook handlers (Stripe, GitHub, etc.)
  • Database triggers and automation
  • Scheduled background jobs
  • API integrations
  • Custom authentication flows
  • Data transformations

1. Setup and Configuration#

Install Supabase CLI#

npm install supabase --save-dev
npx supabase init
npx supabase login
npx supabase link --project-ref your-project-ref

Create Your First Function#

npx supabase functions new hello-world

This creates: supabase/functions/hello-world/index.ts

2. Basic Edge Function Structure#

Simple HTTP Handler#

// supabase/functions/hello-world/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

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

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

With Supabase Client#

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const supabaseClient = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    {
      global: {
        headers: { Authorization: req.headers.get('Authorization')! },
      },
    }
  )

  const { data: { user } } = await supabaseClient.auth.getUser()

  if (!user) {
    return new Response(
      JSON.stringify({ error: 'Unauthorized' }),
      { status: 401, headers: { "Content-Type": "application/json" } }
    )
  }

  const { data, error } = await supabaseClient
    .from('posts')
    .select('*')
    .eq('user_id', user.id)

  if (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    )
  }

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

3. Local Development#

Start Local Functions#

npx supabase functions serve

Test with curl#

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"}'

Test from Next.js#

// app/api/test-function/route.ts
export async function POST(request: Request) {
  const { name } = await request.json()

  const response = await fetch(
    `${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/hello-world`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
      },
      body: JSON.stringify({ name }),
    }
  )

  const data = await response.json()
  return Response.json(data)
}

4. Webhook Handlers#

Stripe Webhook#

// supabase/functions/stripe-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import Stripe from 'https://esm.sh/stripe@11.1.0?target=deno'

const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY') || '', {
  apiVersion: '2023-10-16',
  httpClient: Stripe.createFetchHttpClient(),
})

const cryptoProvider = Stripe.createSubtleCryptoProvider()

serve(async (req) => {
  const signature = req.headers.get('Stripe-Signature')
  const body = await req.text()
  
  let event

  try {
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature!,
      Deno.env.get('STRIPE_WEBHOOK_SECRET')!,
      undefined,
      cryptoProvider
    )
  } catch (err) {
    return new Response(
      JSON.stringify({ error: err.message }),
      { status: 400 }
    )
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object
      
      await supabase
        .from('subscriptions')
        .insert({
          user_id: session.metadata.user_id,
          stripe_customer_id: session.customer,
          stripe_subscription_id: session.subscription,
          status: 'active',
        })
      
      break
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object
      
      await supabase
        .from('subscriptions')
        .update({
          status: subscription.status,
          current_period_end: new Date(subscription.current_period_end * 1000),
        })
        .eq('stripe_subscription_id', subscription.id)
      
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object
      
      await supabase
        .from('subscriptions')
        .update({ status: 'canceled' })
        .eq('stripe_subscription_id', subscription.id)
      
      break
    }
  }

  return new Response(JSON.stringify({ received: true }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

GitHub Webhook#

// supabase/functions/github-webhook/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { crypto } from "https://deno.land/std@0.168.0/crypto/mod.ts"

async function verifySignature(
  payload: string,
  signature: string,
  secret: string
): Promise<boolean> {
  const encoder = new TextEncoder()
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"]
  )
  
  const signed = await crypto.subtle.sign(
    "HMAC",
    key,
    encoder.encode(payload)
  )
  
  const expectedSignature = `sha256=${Array.from(new Uint8Array(signed))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')}`
  
  return signature === expectedSignature
}

serve(async (req) => {
  const signature = req.headers.get('X-Hub-Signature-256')
  const event = req.headers.get('X-GitHub-Event')
  const body = await req.text()

  const isValid = await verifySignature(
    body,
    signature!,
    Deno.env.get('GITHUB_WEBHOOK_SECRET')!
  )

  if (!isValid) {
    return new Response('Invalid signature', { status: 401 })
  }

  const payload = JSON.parse(body)
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  if (event === 'push') {
    await supabase.from('deployments').insert({
      repo: payload.repository.full_name,
      branch: payload.ref.replace('refs/heads/', ''),
      commit_sha: payload.after,
      commit_message: payload.head_commit.message,
      author: payload.head_commit.author.name,
    })
  }

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

5. Database Triggers#

Trigger on Insert#

-- Create function to call edge function
CREATE OR REPLACE FUNCTION trigger_edge_function()
RETURNS TRIGGER AS $$
DECLARE
  request_id bigint;
BEGIN
  SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/on-user-created',
    headers := jsonb_build_object(
      'Content-Type', 'application/json',
      'Authorization', 'Bearer ' || current_setting('app.service_role_key')
    ),
    body := jsonb_build_object(
      'user_id', NEW.id,
      'email', NEW.email
    )
  ) INTO request_id;
  
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create trigger
CREATE TRIGGER on_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW
  EXECUTE FUNCTION trigger_edge_function();

Edge Function Handler#

// supabase/functions/on-user-created/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const { user_id, email } = await req.json()

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Create user profile
  await supabase.from('profiles').insert({
    id: user_id,
    email,
    created_at: new Date().toISOString(),
  })

  // Send welcome email
  await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'welcome@iloveblog.blog',
      to: email,
      subject: 'Welcome!',
      html: '<h1>Welcome to our platform!</h1>',
    }),
  })

  return new Response(JSON.stringify({ success: true }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

6. Scheduled Jobs#

Using pg_cron#

-- Enable pg_cron extension
CREATE EXTENSION IF NOT EXISTS pg_cron;

-- Schedule daily cleanup job
SELECT cron.schedule(
  'daily-cleanup',
  '0 2 * * *', -- Run at 2 AM daily
  $$
  SELECT net.http_post(
    url := 'https://your-project.supabase.co/functions/v1/daily-cleanup',
    headers := jsonb_build_object(
      'Authorization', 'Bearer ' || current_setting('app.service_role_key')
    )
  );
  $$
);

Cleanup Function#

// supabase/functions/daily-cleanup/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  // Delete old sessions
  const { data: deletedSessions } = await supabase
    .from('sessions')
    .delete()
    .lt('expires_at', new Date().toISOString())
    .select()

  // Archive old logs
  const thirtyDaysAgo = new Date()
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)

  const { data: logs } = await supabase
    .from('logs')
    .select('*')
    .lt('created_at', thirtyDaysAgo.toISOString())

  if (logs && logs.length > 0) {
    await supabase.from('logs_archive').insert(logs)
    await supabase
      .from('logs')
      .delete()
      .lt('created_at', thirtyDaysAgo.toISOString())
  }

  return new Response(
    JSON.stringify({
      deleted_sessions: deletedSessions?.length || 0,
      archived_logs: logs?.length || 0,
    }),
    { headers: { 'Content-Type': 'application/json' } }
  )
})

7. Email Processing#

Send Transactional Emails#

// supabase/functions/send-email/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

serve(async (req) => {
  const { to, subject, html } = await req.json()

  const response = await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'noreply@iloveblog.blog',
      to,
      subject,
      html,
    }),
  })

  const data = await response.json()

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

8. Deployment#

Deploy Single Function#

npx supabase functions deploy hello-world

Deploy All Functions#

npx supabase functions deploy

Set Environment Variables#

npx supabase secrets set STRIPE_SECRET_KEY=sk_test_...
npx supabase secrets set RESEND_API_KEY=re_...

List Secrets#

npx supabase secrets list

9. Calling from Next.js#

Client-Side Call#

'use client'

import { createClient } from '@/lib/supabase/client'

export function InvokeFunction() {
  const supabase = createClient()

  async function callFunction() {
    const { data, error } = await supabase.functions.invoke('hello-world', {
      body: { name: 'World' },
    })

    if (error) {
      console.error('Error:', error)
      return
    }

    console.log('Response:', data)
  }

  return <button onClick={callFunction}>Call Function</button>
}

Server-Side Call#

// app/api/invoke/route.ts
import { createClient } from '@/lib/supabase/server'

export async function POST(request: Request) {
  const supabase = createClient()
  const { name } = await request.json()

  const { data, error } = await supabase.functions.invoke('hello-world', {
    body: { name },
  })

  if (error) {
    return Response.json({ error: error.message }, { status: 500 })
  }

  return Response.json(data)
}

10. Error Handling and Logging#

Structured Logging#

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"

function log(level: string, message: string, meta?: any) {
  console.log(JSON.stringify({
    timestamp: new Date().toISOString(),
    level,
    message,
    ...meta,
  }))
}

serve(async (req) => {
  try {
    log('info', 'Function invoked', { method: req.method })

    const { data } = await req.json()
    
    // Process data
    
    log('info', 'Function completed successfully')
    
    return new Response(JSON.stringify({ success: true }), {
      headers: { 'Content-Type': 'application/json' },
    })
  } catch (error) {
    log('error', 'Function failed', { error: error.message })
    
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    )
  }
})

11. Best Practices#

Security#

  • Never expose service role key to clients
  • Validate all inputs
  • Use environment variables for secrets
  • Implement rate limiting
  • Verify webhook signatures

Performance#

  • Keep functions small and focused
  • Use connection pooling for database
  • Cache responses when possible
  • Minimize cold start time
  • Use streaming for large responses

Monitoring#

  • Log all errors with context
  • Track function invocations
  • Monitor execution time
  • Set up alerts for failures
  • Use structured logging

12. Common Use Cases#

Image Processing#

import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

serve(async (req) => {
  const { imageUrl } = await req.json()

  // Download image
  const response = await fetch(imageUrl)
  const imageBuffer = await response.arrayBuffer()

  // Process image (resize, compress, etc.)
  // ... image processing logic ...

  // Upload to Supabase Storage
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
  )

  const fileName = `processed-${Date.now()}.jpg`
  const { data, error } = await supabase.storage
    .from('images')
    .upload(fileName, imageBuffer, {
      contentType: 'image/jpeg',
    })

  if (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500 }
    )
  }

  const { data: { publicUrl } } = supabase.storage
    .from('images')
    .getPublicUrl(fileName)

  return new Response(
    JSON.stringify({ url: publicUrl }),
    { headers: { 'Content-Type': 'application/json' } }
  )
})

FAQ#

What are Supabase Edge Functions?#

Supabase Edge Functions are serverless functions that run on Deno Deploy, executing globally at the edge close to your users. They provide sub-50ms cold starts, TypeScript support, and direct access to your Supabase database without managing servers.

How do Edge Functions differ from Next.js API routes?#

Edge Functions run on Deno Deploy globally, while Next.js API routes run on your deployment platform (like Vercel). Edge Functions are better for webhooks, database triggers, and scheduled jobs, while API routes are better for application-specific logic that needs tight integration with your Next.js app.

Can I use npm packages in Edge Functions?#

Yes, but you must use Deno-compatible packages from esm.sh or deno.land. Most popular npm packages have Deno equivalents. For example, use import Stripe from 'https://esm.sh/stripe@11.1.0?target=deno' instead of npm install.

How do I test Edge Functions locally?#

Use npx supabase functions serve to run functions locally. They'll be available at http://localhost:54321/functions/v1/your-function-name. You can test with curl, Postman, or directly from your Next.js app.

Are Edge Functions free?#

Supabase provides 500,000 Edge Function invocations per month on the free tier. Beyond that, pricing is $2 per 1 million invocations. This is very cost-effective for most applications.

How do I handle secrets in Edge Functions?#

Use npx supabase secrets set KEY=value to set environment variables. Access them in your function with Deno.env.get('KEY'). Never commit secrets to your repository.

Can Edge Functions access my database?#

Yes! Edge Functions have full access to your Supabase database using the Supabase client. You can use either the anon key (with RLS) or service role key (bypasses RLS) depending on your needs.

What's the maximum execution time for Edge Functions?#

Edge Functions have a default timeout of 150 seconds. This is sufficient for most use cases including webhook processing, email sending, and data transformations.

How do I deploy Edge Functions to production?#

Use npx supabase functions deploy function-name to deploy a single function, or npx supabase functions deploy to deploy all functions. Deployment typically takes 10-30 seconds.

Can I use Edge Functions with other frameworks besides Next.js?#

Absolutely! Edge Functions are framework-agnostic. You can call them from any application (React, Vue, Angular, mobile apps) using standard HTTP requests or the Supabase client library.

Frequently Asked Questions (FAQ)#

What are Supabase Edge Functions?#

Supabase Edge Functions are serverless functions that run on Deno Deploy, executing globally at the edge close to your users. They provide sub-50ms cold starts, direct Supabase database access, and seamless integration with your Next.js applications without managing servers.

How do Supabase Edge Functions differ from Next.js API routes?#

Supabase Edge Functions run on Deno Deploy (globally distributed), while Next.js API routes run on your deployment platform (Vercel, AWS, etc.). Edge Functions are better for webhooks, database triggers, and scheduled jobs, while API routes are better for application-specific logic that needs tight integration with your Next.js app.

Can I use npm packages in Supabase Edge Functions?#

Yes, but you need to use Deno-compatible packages from esm.sh or deno.land. For example: import Stripe from 'https://esm.sh/stripe@11.1.0?target=deno'. Most popular npm packages have Deno-compatible versions.

How do I test Edge Functions locally?#

Use the Supabase CLI: npx supabase functions serve to start a local development server. Then test with curl or from your Next.js app pointing to http://localhost:54321/functions/v1/your-function.

What's the cold start time for Supabase Edge Functions?#

Supabase Edge Functions typically have sub-50ms cold starts due to Deno's fast startup time and global distribution on Deno Deploy. This is significantly faster than traditional serverless functions.

How do I handle secrets in Edge Functions?#

Use the Supabase CLI to set secrets: npx supabase secrets set API_KEY=value. Access them in your function with Deno.env.get('API_KEY'). Never hardcode secrets in your function code.

Can Edge Functions access my Supabase database?#

Yes, Edge Functions have full access to your Supabase database using the Supabase client. You can use either the anon key (with RLS) or service role key (bypasses RLS) depending on your needs.

How much do Supabase Edge Functions cost?#

Supabase Edge Functions are included in all plans with generous limits. The free tier includes 500K function invocations per month. Pro plan includes 2M invocations. Additional invocations cost $2 per 1M requests.

Can I use Edge Functions for scheduled jobs?#

Yes, combine Edge Functions with PostgreSQL's pg_cron extension to schedule jobs. Create a cron job that calls your Edge Function via HTTP at specified intervals (hourly, daily, etc.).

How do I deploy Edge Functions to production?#

Use the Supabase CLI: npx supabase functions deploy function-name. This deploys your function to Deno Deploy globally. Set environment variables with npx supabase secrets set KEY=value.

Can Edge Functions handle file uploads?#

Yes, Edge Functions can receive file uploads and store them in Supabase Storage. Parse the multipart form data, then use the Supabase client to upload to storage buckets.

What's the maximum execution time for Edge Functions?#

Edge Functions have a default timeout of 150 seconds. This is configurable and sufficient for most use cases including webhook processing, email sending, and data transformations.

Conclusion#

Supabase Edge Functions provide a powerful serverless platform for extending your Next.js applications. With global distribution, seamless database integration, and simple deployment, they're perfect for webhooks, background jobs, and API integrations.

Start with simple HTTP handlers, then add database triggers, scheduled jobs, and complex workflows as your application grows.

Frequently Asked Questions

|

Have more questions? Contact us