Edge Functions

Deno で動く Supabase 内のサーバ側関数。 外部 API 呼び出し、認証検証後の処理、Webhook 受信、Cron 用途まで。

特徴

新規作成

supabase functions new hello
# supabase/functions/hello/index.ts が作られる

最小例

// supabase/functions/hello/index.ts
Deno.serve(async (req) => {
  const { name } = await req.json()
  return new Response(JSON.stringify({ message: `Hello, ${name}!` }), {
    headers: { "Content-Type": "application/json" },
  })
})

ローカルで動かす

supabase functions serve hello

# 別ターミナルで
curl -X POST http://localhost:54321/functions/v1/hello \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SUPABASE_ANON_KEY" \
  -d '{"name":"world"}'

デプロイ

supabase functions deploy hello

# 認証無しで呼べるようにする
supabase functions deploy hello --no-verify-jwt

呼び出し

// supabase-js から
const { data, error } = await supabase.functions.invoke("hello", {
  body: { name: "Alice" },
})

// 直 fetch
await fetch(`${URL}/functions/v1/hello`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${session.access_token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Alice" }),
})

環境変数

Supabase が自動で注入する変数:

独自シークレット

supabase secrets set OPENAI_API_KEY=sk-...
supabase secrets list
supabase secrets unset OPENAI_API_KEY

# .env を一括投入
supabase secrets set --env-file ./supabase/.env
const apiKey = Deno.env.get("OPENAI_API_KEY")!

DB に接続

supabase-js(推奨)

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

Deno.serve(async (req) => {
  // 1) ユーザ権限で(RLS 適用)
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    {
      global: { headers: { Authorization: req.headers.get("Authorization")! } },
    }
  )

  const { data, error } = await supabase.from("posts").select()
  return new Response(JSON.stringify({ data, error }))
})

service_role で(管理者として)

const admin = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
)

// RLS をバイパスして全件操作できる

認証ユーザの取得

Deno.serve(async (req) => {
  const supabase = createClient(URL, ANON_KEY, {
    global: { headers: { Authorization: req.headers.get("Authorization")! } },
  })
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return new Response("Unauthorized", { status: 401 })

  // user.id で何かする
})

CORS 対応

const cors = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
  "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: cors })

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

典型ユースケース

1. 外部 API 呼び出し(API キーを隠す)

Deno.serve(async (req) => {
  const { prompt } = await req.json()
  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": Deno.env.get("ANTHROPIC_API_KEY")!,
      "anthropic-version": "2023-06-01",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    }),
  })
  return new Response(res.body, { headers: { "Content-Type": "application/json" } })
})

2. Webhook 受信(Stripe 等)

import Stripe from "https://esm.sh/stripe@14"
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET")!)

Deno.serve(async (req) => {
  const sig = req.headers.get("stripe-signature")!
  const body = await req.text()
  const event = await stripe.webhooks.constructEventAsync(
    body, sig, Deno.env.get("STRIPE_WEBHOOK_SECRET")!
  )

  if (event.type === "checkout.session.completed") {
    // DB 更新
  }
  return new Response("ok")
})

3. Auth Hook(ユーザ作成時のフック)

Supabase の Auth Hook 機能で、サインアップ時に自動で関数を呼べる:

// HTTP Hook として登録
Deno.serve(async (req) => {
  const { user_id, email, claims } = await req.json()
  // Slack 通知 / DB に追加情報書き込み 等
  return new Response(JSON.stringify({ claims }), {
    headers: { "Content-Type": "application/json" },
  })
})

4. Cron / 定期ジョブ

Supabase 内の pg_cron 拡張で、定期的に Edge Function を呼び出せる:

select cron.schedule(
  'daily-report',
  '0 0 * * *',  -- 毎日 0 時
  $$
  select net.http_post(
    'https://xxxx.supabase.co/functions/v1/daily-report',
    '{}'::jsonb,
    '{"Authorization": "Bearer SERVICE_KEY"}'::jsonb
  );
  $$
);

5. ストリーミング(LLM 等)

Deno.serve(async () => {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 10; i++) {
        controller.enqueue(new TextEncoder().encode(`chunk ${i}\n`))
        await new Promise((r) => setTimeout(r, 200))
      }
      controller.close()
    },
  })
  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream" },
  })
})

npm モジュール

import { z } from "npm:zod@3"
import postgres from "npm:postgres"
import { Hono } from "jsr:@hono/hono"

const app = new Hono()
app.get("/", (c) => c.json({ ok: true }))
Deno.serve(app.fetch)

共有コード(_shared)

複数の関数で使う型やヘルパは _shared/ に置く:

supabase/functions/
├── _shared/
│   └── cors.ts
├── hello/
│   └── index.ts
└── webhook/
    └── index.ts
      

制限

ロギング

console.log("info")
console.error("error", err)
// Dashboard → Edge Functions → Logs で見える
// supabase functions logs hello でも

OpenAI / Anthropic ラッパ例

フロントから直接 LLM プロバイダを叩くと API キーが漏れる。Edge Functions でバックエンドを薄く挟む:

// supabase/functions/chat/index.ts
import Anthropic from "npm:@anthropic-ai/sdk"

const client = new Anthropic({ apiKey: Deno.env.get("ANTHROPIC_API_KEY")! })

Deno.serve(async (req) => {
  // ユーザ認証
  const supabase = createClient(URL, ANON_KEY, {
    global: { headers: { Authorization: req.headers.get("Authorization")! } },
  })
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return new Response("Unauthorized", { status: 401 })

  const { messages } = await req.json()

  // ストリーミング
  const stream = await client.messages.stream({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages,
  })

  return new Response(stream.toReadableStream(), {
    headers: { "Content-Type": "text/event-stream" },
  })
})

失敗パターン

症状原因
401verify-jwt が ON で Authorization ヘッダ欠如
CORS エラーOPTIONS 応答忘れ
環境変数 undefinedsecrets set 忘れ / デプロイ忘れ
Cold start で遅い軽量化、定期 ping、Pro へ
Edge で動かない依存npm ライブラリのfs / Buffer依存に注意
関数間で値が共有されない各実行は独立。共有は DB 経由

セルフホスト

オープンソース版はedge-runtime(Deno ベース)が同梱され、Docker 内で動く。

使うべき場面

API キーを隠す / 認証後の処理 / Webhook / 定期ジョブに最適。 単に DB を読み書きするだけなら supabase-js だけで完結するので不要。