Supabase Edge Functions は、Supabase のプロジェクトに付いてくるサーバーレスの実行環境です。TypeScript のファイルを1つ置いて supabase functions deploy を叩けば、認証もデータベース接続も揃った状態で HTTP エンドポイントが立ち上がります。ただし Node.js の感覚で書くと、依存パッケージの読み込み方でも、鍵の置き場所でも、実行時間の上限でも詰まりやすい構造です。この記事では、Deno ベースの実行環境が何を許して何を許さないのか、作成からデプロイまでのコマンド、シークレットの持たせ方、データベースやWebhookから呼び出す4つの経路、そして見落としやすい上限と課金単位までを、2026年8月時点の公式ドキュメントに沿って実装順に並べます。
まとめ:Edge Functionsの性格と着手前に決める3点
先に結論を出します。Edge Functions は「短時間で終わる処理を、データベースの近くに置いたまま外部と接続する」ための道具です。CPU 時間の上限が1リクエスト2秒、メモリが256MB という設計値からわかるように、重い計算やバッチ処理をここに置く前提にはなっていません。逆に、Webhook の受信、外部 API の呼び出し、認証済みユーザーだけに許す小さな処理といった用途では、サーバーを別に用意する必要がなくなります。
着手前に決めるのは3点です。第一に鍵の置き場所で、service_role の鍵を関数の中で使うのか、行レベルセキュリティに任せるのかを先に決めます。第二に呼び出し元で、クライアントから直接叩くのか、データベースのトリガー経由にするのかによって認証の設計が変わります。第三に処理時間の見込みで、応答を待たせずに続きを走らせたいなら EdgeRuntime.waitUntil を前提に組み立ててください。
Deno 2系で動く実行環境と依存パッケージを安全に扱うための基本
Edge Functions の土台は Supabase Edge Runtime という Deno 互換のランタイムです。Node.js との差は、ビルド工程と依存の解決方法にはっきり出ます。
ビルド工程を持たないTypeScript実行とアイソレートごとの分離方式
.ts ファイルをそのまま実行できるため、トランスパイルの設定を書く工程がありません。エントリポイントは Deno.serve() にハンドラを渡す形が基本で、受け取るのは標準の Request、返すのは標準の Response です。ブラウザで使う fetch がそのまま動くので、外部 API を叩くコードは書き換えずに持ち込めます。
Deno.serve(async (req) => {
const body = await req.json()
return new Response(
JSON.stringify({ message: "Hello " + body.name }),
{ headers: { "Content-Type": "application/json" } }
)
})
実行の単位はコンテナではなくアイソレートで、全リージョンが2026年8月時点で Deno 2系(公開情報では2.1系に相当)を走らせています。起動が軽い構造ではありますが、後述するようにコールドスタートが完全に消えるわけではありません。
npm:指定子とnode:モジュールを安全に読み込むための設定方法
依存の解決は package.json ではなく、インポート文の指定子で行います。npm のパッケージは npm:、Node.js の標準モジュールは node: を前置します。
import Stripe from "npm:stripe"
import { createHmac } from "node:crypto"
プライベートレジストリのパッケージも読み込めますが、公開情報ではその機能に Supabase CLI v1.207.9 以降が必要とされています。バンドルサイズの上限は CLI からのデプロイで20MB、サーバー側バンドルで5MB なので、Node.js 向けの巨大な SDK を丸ごと持ち込む構成は避けたほうが無難です。
自動で入る環境変数と権限に応じたクライアント生成方法の選び方
プロジェクト URL と各キーは、こちらで設定しなくても環境変数として渡ってきます。SUPABASE_URL、SUPABASE_ANON_KEY、SUPABASE_SERVICE_ROLE_KEY、SUPABASE_DB_URL の4つが予約済みで、同名のシークレットを後から作って上書きすることはできません。
関数の中でどちらのキーを使うかは、認可の設計そのものです。呼び出し元のユーザー権限で動かすなら anon キーのクライアントに Authorization ヘッダを引き継ぎ、管理処理として動かす場合は service_role が選択肢です。service_role は行レベルセキュリティを素通りするため、この判断を曖昧にすると権限の穴になります。ポリシーの書き方そのものはSupabase RLSの設定手順とAuth連携ポリシー設計で扱っています。
Supabase CLIで作成からデプロイまで進めるコマンドと環境別の設定手順
ローカルで書いて本番へ上げるまでの流れは、CLI の4コマンドでほぼ完結します。
functions newとserveで関数を作成してローカル実行する手順
雛形の生成と起動は次の順です。serve は本番と同じランタイムをローカルに立てるため、Docker が動く環境が前提になります。
supabase login
supabase link --project-ref <project-ref>
supabase functions new hello-world
supabase functions serve hello-world
生成されるのは supabase/functions/<関数名>/index.ts です。ディレクトリ名がそのままエンドポイントのパスになるので、命名は後から変えにくい点を意識してください。ローカルスタック全体の立ち上げやマイグレーションの運用は、Supabase CLIとローカル開発環境の構築手順で扱っています。
deployとconfig.tomlでJWT検証を環境別に設定する手順
デプロイは関数名を付ければ1本だけ、省略すれば配下すべてが対象です。
supabase functions deploy hello-world
supabase functions deploy --project-ref <project-ref>
既定では呼び出しに有効な JWT が要求されます。Stripe や GitHub のような外部サービスからの Webhook を受ける関数は JWT を持たないので、この検証を切る必要があります。コマンドのフラグで都度指定するのではなく、supabase/config.toml に書いてリポジトリで管理する形が公式の推奨です。
[functions.stripe-webhook]
verify_jwt = false
検証を切った関数は誰でも叩ける状態になります。署名ヘッダの検証をハンドラの先頭に置く、という組み合わせで運用してください。
CI/CDへの組み込みとアクセストークンを安全に管理する方法
GitHub Actions などから流す場合は、対話ログインの代わりに SUPABASE_ACCESS_TOKEN を環境変数で渡し、supabase functions deploy --project-ref $PROJECT_ID を実行します。失敗の切り分けには --debug を付けた出力が役に立ちます。ステージングと本番でプロジェクトが分かれている構成なら、project-ref をブランチごとの変数に持たせるのが素直な形です。
シークレット管理の手順と本番環境へ持たせてよい値の判断基準を定める方法
外部 API の鍵をコードに書かずに渡す仕組みが用意されています。ローカルと本番で置き場所が違う点だけ押さえてください。
secrets setと.env.localを本番とローカルで使い分ける手順
本番の関数へ渡す値は supabase secrets で登録します。個別指定とファイル一括のどちらも通ります。
supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx
supabase secrets set --env-file ./supabase/functions/.env
supabase secrets list
ローカル実行時は supabase/functions/.env.local が読まれます。この2系統を混ぜると、本番だけ値が入っていない状態に気付きにくくなる原因です。関数の中では Deno.env.get("STRIPE_SECRET_KEY") で取り出す形になり、未設定時は undefined が返るので、起動直後に存在確認を入れておくと事故が早く見つかります。
シークレットの登録上限とクライアント側へ出してよい値の判断基準
公式の制限では、1プロジェクトあたりのシークレット数が100、名前が最大256文字、1件のサイズが48KiB までです。証明書のような大きな値を詰め込む用途には向きません。
フロントエンドに露出してよいのはプロジェクト URL と anon キーだけで、service_role の鍵はクライアントへ渡してはいけません。Edge Functions を挟む価値の1つは、この鍵をサーバー側に留めたまま処理を実行できる点にあります。キーの権限差と漏洩時の対処はSupabase APIキーの管理を扱った記事で扱っています。
関数を呼び出す4つの経路と認証方式を選ぶための実装時の判断基準
呼び出し元によって、認証の付け方と失敗時の見え方が変わります。
クライアントSDKのinvokeとHTTP直叩きを認証方式で選ぶ基準
アプリからは SDK の functions.invoke が最短です。ログイン中のセッションから Authorization ヘッダが自動で載るため、関数側でユーザーを特定できます。
const { data, error } = await supabase.functions.invoke("hello-world", {
body: { name: "issoh" },
})
curl で叩く場合のエンドポイントは https://<project-ref>.supabase.co/functions/v1/<関数名> です。ブラウザから直接呼ぶなら、CORS のプリフライトへの応答をハンドラに足す必要があります。
DBトリガーとDatabase Webhooksからの呼び出し
テーブルの変更をきっかけに関数を動かす経路は2通りあります。ダッシュボードの Database Webhooks で対象テーブルとイベントを選ぶ方法と、AFTER トリガーの中から pg_net 拡張の net.http_post を呼ぶ方法です。前者は内部的にも後者の仕組みを使っており、送信は非同期で行われます。
select net.http_post(
url := 'https://<project-ref>.supabase.co/functions/v1/notify',
headers := jsonb_build_object(
'Content-Type', 'application/json',
'Authorization', 'Bearer ' || current_setting('app.service_key')
),
body := jsonb_build_object('id', new.id)
);
ここで押さえておきたいのは、送信が非同期であるためトランザクションのロールバックと連動しないことです。呼び出しが飛んだあとに更新が巻き戻る可能性を織り込んだ設計にしてください。件数が多い、あるいは再試行を効かせたい処理なら、pgmqでPostgreSQLをキュー化する構成を挟んで関数側で取り出す形のほうが安定します。画面へ即時に反映させたい通知は、Supabase Realtimeの実装で扱った購読側の経路と組み合わせる形になります。
Cronによる定期実行と外部Webhookを安全に受信する設定手順
定期実行はダッシュボードの Cron から関数を選ぶか、pg_cron のスケジュールから net.http_post を呼ぶ形で組めます。分単位より短い間隔を刻む用途には向いていません。
外部サービスからの Webhook 受信は、前述の verify_jwt = false と署名検証の組み合わせが定型です。送信元の再送仕様も確認してください。多くのサービスは応答が遅いと再送するため、重い処理を待たせると同じイベントが二重に届きます。受信直後に200を返し、続きはバックグラウンドタスクへ回す形が安全です。
見落としやすい上限と課金単位から実装設計を決めるための判断基準
ここが設計の分かれ目になります。数値は2026年8月時点の公式ドキュメントの記載です。
メモリ・CPU時間・実行時間の3種類の上限を踏まえた設計基準
| 項目 | Free | 有料プラン |
|---|---|---|
| メモリ | 256MB | 256MB |
| CPU時間/リクエスト | 2秒 | 2秒 |
| 実行時間の上限 | 150秒 | 400秒 |
| 関数数/プロジェクト | 100 | Pro 1,000/Team 2,000 |
混同しやすいのは CPU 時間と実行時間の違いです。CPU 時間はプロセスが計算に使った時間で、外部 API の応答待ちは含みません。つまり2秒という値は「待つのは構わないが、自前で回す計算は短く済ませる」という制約です。加えてアイドル状態が150秒続くと504が返ります。ポート25と587への外向き接続は禁止されているため、SMTP で直接メールを送る実装はできません。
waitUntilによるバックグラウンド処理と/tmpの扱い
応答を先に返して処理を続けたい場合は EdgeRuntime.waitUntil に Promise を渡します。渡された処理が終わるまで関数は終了しません。継続できる時間は Free で150秒、有料プランで400秒です。
Deno.serve((req) => {
EdgeRuntime.waitUntil(heavyJob(req))
return new Response("accepted", { status: 202 })
})
一時ファイルは /tmp に書けます。node:fs でも Deno の API でも触れますが、この領域は呼び出しごとにリセットされるため、同じ呼び出しの中でしか読み戻せません。アップロードされたファイルを加工して Storage へ渡すような使い方が想定される範囲です。
課金は呼び出し回数で決まり実行時間を含まない料金体系の考え方
料金ページの記載では、Edge Functions の課金対象は呼び出し回数です。Free は月50万回が含まれ、Pro と Team は200万回が含まれたうえで、超過分が100万回あたり2ドルとされています。実行時間や GB 秒での加算は示されていません。
この課金モデルは、設計の向き先を変える要因です。実行時間で課金される環境では処理を短く刻むのが有利ですが、回数課金では逆で、細かい関数を何度も呼ぶ構成のほうがコストが伸びます。1画面の描画で3つの関数を叩く設計と、1つにまとめて内部で分岐する設計とでは、請求が3倍違う計算になります。呼び出し回数の見積もりは、実装前に一度置いてください。
Edge Functionsを採用する条件と自前サーバーへ切り替える判断基準
ここまでの制約を踏まえて、境界を言い切ります。
Edge Functionsで処理を完結させてよい3つの採用条件
次の3つを同時に満たすなら、Edge Functions に寄せる判断で問題ありません。第一に、1リクエストの計算量が軽く、重い部分は外部 API かデータベース側にある。第二に、認証が Supabase Auth で完結しており、呼び出し元が発行する JWT を検証すれば済む。第三に、状態を関数の中に持たず、データベースか Storage に置ける。
この3つに収まる典型は、Webhook の受信と正規化、外部 API を叩いてから書き込む処理、そして service_role が必要な少数の管理操作です。専用のサーバーを立てて監視まで用意する手間と比べれば、運用の負担がはっきり小さくなります。
Edge Functionsを見送って別環境を選ぶべき4つの判断場面
逆に、次のいずれかに当てはまるなら別の置き場所を検討してください。第一に、1リクエストの計算が2秒の CPU 時間に収まらない画像処理や集計。第二に、常駐プロセスや大きなメモリを前提とする処理で、256MB では足りない場合。第三に、Node.js 固有の依存が深く npm: 指定子で解決しきれないライブラリを使う場合。第四に、呼び出し回数が月数千万に達し、回数課金が実行時間課金より不利になる場合です。
置き換え先の候補は、用途ごとに異なる構成です。トリガー設計を細かく組みたいならCloud Functions for Firebaseのトリガー設計、配信の地理的な分散と実行時間の柔軟さを取るならCloudflare Workersの対応言語と料金が比較対象になります。データベースを含む構成全体をどこに置くかという話になるなら、インフラ構築(AWS・Google Cloud・Azure)の相談として扱っています。
よくある質問
Node.jsのコードをそのまま持ち込めますか?
一部は動きますが、無修正での移植は前提にしないでください。npm のパッケージは npm:、標準モジュールは node: の前置が必要で、__dirname のような Node.js 固有のグローバルやファイルシステム前提の処理は書き換えが要ります。バンドル上限もあるため、依存の重いコードは先に切り出しておくほうが早く済みます。
コールドスタートはどのくらい起きますか?
アイソレート方式のため起動は軽い側ですが、公式ドキュメントもコールドスタートの発生自体は認めています。呼び出しが間欠的な用途では、初回応答が遅れる前提で設計してください。応答時間の要件が厳しい経路では、定期的に叩いて温める運用か、そもそも常駐する実行環境を選ぶ判断になります。
service_roleキーを関数の中で使ってよいですか?
用途を限れば問題ありません。ただしこの鍵は行レベルセキュリティを素通りするため、使う関数では認可の判定をコード側で書く責任が生じます。呼び出し元のユーザー権限で足りる処理なら、anon キーのクライアントに JWT を引き継いでポリシーに任せる形のほうが安全です。
実行時間の上限に引っかかる処理はどう扱いますか?
先に202を返して EdgeRuntime.waitUntil で続きを走らせる形にすると、有料プランで400秒まで伸ばせます。それでも収まらないなら、キューへ積んで別の実行環境で処理する構成へ切り替えてください。関数の中でループを回して粘る実装は、CPU 時間2秒の壁にも当たります。
ローカルで動くのに本番で500が返るのはなぜですか?
典型はシークレットの未設定です。ローカルは .env.local を読むのに対し、本番は supabase secrets set で登録した値を読むため、片方だけ入っている状態が起きます。supabase secrets list で登録名を確認し、それでも切り分かない場合はダッシュボードのログで例外の内容を見てください。
関連記事
- SupabaseとFirebaseの違いを徹底比較|料金・認証・どっちを選ぶか:製品全体の機能範囲と料金の考え方を整理しています。
- Supabase RLSの設定手順とAuth連携ポリシー設計|有効化漏れの検知まで:関数から触るテーブルの認可設計を扱っています。
- Supabase Realtimeの実装|3方式の使い分けとRLS併用・同時接続の上限:変更を画面へ届ける購読側の実装を解説しています。
- Cloud Functions for Firebaseとは|トリガー設計と第2世代の実装判断:同種のサーバーレス関数との設計差が比較できます。
- Cloudflare Workersとは?対応言語・無料枠・使い方・料金を総まとめ:エッジ実行環境としての料金と制約の違いを扱っています。
- pgmqとは?PostgreSQLをメッセージキューにする拡張の使い方:関数の前段にキューを置く非同期構成が参考になります。