跳至主要內容

Supabase Edge Functions 開發實戰指南

Supabase Edge Functions 開發實戰指南

Supabase Edge Functions 是基於 Deno 運行的 serverless 函式,部署在全球各地的邊緣節點,讓 API 邏輯可以在距離使用者最近的伺服器上執行。這篇文章介紹如何從零開始開發和部署 Edge Functions。

環境設定

# 安裝 Supabase CLI
npm install -g supabase

# 初始化專案
supabase init

# 登入
supabase login

# 連結到你的 Supabase 專案
supabase link --project-ref your-project-ref

建立第一個 Edge Function

supabase functions new hello-world

這會建立 supabase/functions/hello-world/index.ts

import "jsr:@supabase/functions-js/edge-runtime.d.ts"

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" } },
  )
})

本地開發與測試

# 啟動本地 Supabase 環境
supabase start

# 啟動 Edge Functions 本地伺服器
supabase functions serve hello-world --env-file .env.local

# 測試
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":"台灣"}' 

存取 Supabase 資料庫

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

Deno.serve(async (req: Request) => {
  // 從請求 header 取得 JWT
  const authHeader = req.headers.get('Authorization')!
  
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL') ?? '',
    Deno.env.get('SUPABASE_ANON_KEY') ?? '',
    { global: { headers: { Authorization: authHeader } } }
  )

  // 查詢資料(會套用 RLS 規則)
  const { data: articles, error } = await supabase
    .from('articles')
    .select('id, title, published_at')
    .eq('is_published', true)
    .order('published_at', { ascending: false })
    .limit(10)

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

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

使用 Service Role(繞過 RLS)

const supabaseAdmin = createClient(
  Deno.env.get('SUPABASE_URL') ?? '',
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
)

// 這個 client 可以讀寫所有資料,不受 RLS 限制
// 只在有必要時使用,注意安全性

處理 Webhook

一個常見的用途是接收第三方 Webhook:

import { createClient } from 'jsr:@supabase/supabase-js@2'
import { crypto } from "jsr:@std/crypto"

const WEBHOOK_SECRET = Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''

async function verifyStripeWebhook(payload: string, signature: string): Promise<boolean> {
  const parts = signature.split(',');
  const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
  const sig = parts.find(p => p.startsWith('v1='))?.split('=')[1];
  
  if (!timestamp || !sig) return false;

  const signedPayload = `${timestamp}.${payload}`;
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(WEBHOOK_SECRET),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const signatureBuffer = await crypto.subtle.sign(
    'HMAC',
    key,
    new TextEncoder().encode(signedPayload)
  );
  
  const expectedSig = Array.from(new Uint8Array(signatureBuffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
  
  return expectedSig === sig;
}

Deno.serve(async (req: Request) => {
  const signature = req.headers.get('stripe-signature') ?? ''
  const payload = await req.text()

  if (!await verifyStripeWebhook(payload, signature)) {
    return new Response('Unauthorized', { status: 401 })
  }

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

  switch (event.type) {
    case 'payment_intent.succeeded': {
      const { error } = await supabase
        .from('orders')
        .update({ status: 'paid' })
        .eq('stripe_payment_intent_id', event.data.object.id)
      
      if (error) console.error('更新訂單失敗:', error)
      break
    }
    default:
      console.log(`未處理的事件類型:${event.type}`)
  }

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

環境變數管理

# 設定 secret(加密儲存)
supabase secrets set MY_API_KEY=sk-...

# 批次設定
supabase secrets set --env-file .env.production

# 查看所有 secrets(只顯示名稱)
supabase secrets list

本地開發使用 .env.local

# .env.local
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=your-local-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-local-service-role-key
MY_API_KEY=test-key

部署

# 部署單一 function
supabase functions deploy hello-world

# 部署所有 functions
supabase functions deploy

# 不驗證 JWT(公開 API)
supabase functions deploy hello-world --no-verify-jwt

CORS 處理

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

Deno.serve(async (req: Request) => {
  // 處理 preflight 請求
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  try {
    // 你的邏輯
    const data = { message: 'Hello' }
    return new Response(
      JSON.stringify(data),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})

Edge Functions 搭配 Supabase 的其他功能(Auth、Database、Storage)可以快速搭建出功能完整的後端服務,而且不需要管理任何伺服器基礎設施,非常適合中小型專案使用。

分享這篇文章