はじめに
Cloudflare Pages Functions で Better Auth を使うとき、nodejs_compat を有効にするべきかはかなり悩みやすいポイントです。
特に、Cloudflare Workers らしく Node 互換を使わずに構成したい場合は、できるだけ nodejs_compat を避けたくなります。一方で、認証ライブラリの内部実装まで含めると、Node API に依存していることもあります。
今回、Cloudflare Pages + Better Auth + Turso + React という構成のアプリで、開発環境のサインアップ時に 500 エラーが発生しました。調査した結果、Better Auth の内部で node:async_hooks を読みに行っていたことが原因でした。
この記事では、この問題の整理と、nodejs_compat を採用するべきかどうかの判断基準をまとめます。
起きた問題
開発環境で wrangler pages dev を使って /login を開き、Create Account を押すと、画面上は「アカウント作成に失敗しました」と表示されました。
ブラウザの Network を見ると、次のような失敗が出ていました。
GET /api/auth/get-sessionが500POST /api/auth/sign-up/emailが500
さらに wrangler 側のログには、次のエラーが出ていました。
Error: No such module "node:async_hooks"
つまり、フォーム入力やフロントエンドの見た目ではなく、Better Auth のサーバー側ハンドラ自体が Cloudflare Pages Functions 上で落ちていた、ということです。
なぜ node:async_hooks エラーが起きたのか
今回のアプリでは、Cloudflare Pages Functions をできるだけ Workers ネイティブに寄せるため、wrangler.toml に nodejs_compat を入れていませんでした。
一方で、インストールされていた Better Auth は内部的に @better-auth/core/async_hooks を経由して node:async_hooks を使う経路を持っていました。
その結果、Cloudflare Pages Functions 側で Node 互換が有効になっていないため、ここで 500 エラーになっていました。
整理すると、こういう構図です。
- アプリ側の方針: Cloudflare Workers ネイティブで行きたい
- ライブラリ側の実態: Node API を前提にする経路がある
- 結果: Better Auth の認証ハンドラが Cloudflare Pages Functions 上で落ちる
同じ技術スタックでも動くプロジェクトと動かないプロジェクトがあった理由
比較対象の別プロジェクトを確認すると、wrangler.toml に次の設定が入っていました。
compatibility_flags = ["nodejs_compat"]
つまり、そのプロジェクトは最初から Better Auth を Node 互換込みで動かす前提でした。
この差によって、同じ Cloudflare Pages + Better Auth 系の構成でも結果が変わっていました。
nodejs_compatあり -> ログインできるnodejs_compatなし ->node:async_hooksで 500
ここで分かったのは、「同じ技術スタックに見えても、認証実行環境の前提が違うと挙動も変わる」ということです。
検討した2つの対応案
今回の対応方針としては、大きく 2 案ありました。
1. nodejs_compat を有効にする
wrangler.toml に compatibility_flags = ["nodejs_compat"] を追加して、Better Auth が要求する Node API を許可する方法です。
メリット:
- 最短で直る可能性が高い
- Better Auth の内部実装差分を追いかけなくてよい
- ライブラリアップデートに対して比較的強い
- 同じ認証基盤を使う他プロジェクトと構成を揃えやすい
デメリット:
- Workers ネイティブに厳密に寄せる方針は弱まる
- 今後、Node 前提のコードを入れやすくなる
2. @better-auth/core/async_hooks を pure 実装へ逃がす
Vite や bundler 側で alias を張って、node:async_hooks を使わない実装へ誘導する方法です。
メリット:
- Workers ネイティブ寄りの構成を維持できる
- Node 互換を広げずに済む
デメリット:
- Better Auth の内部構造に依存する
- ライブラリアップデートで壊れやすい
- 依存内部を知っていないと保守しづらい
- 問題の再発リスクが残る
結論: Cloudflare Pages + Better Auth では nodejs_compat の方が現実的
今回のように、目的が「まず開発環境でログインとログイン後画面の確認を進めること」である場合は、nodejs_compat を採用する方が合理的だと判断しました。
理由は次のとおりです。
- Better Auth の実装実態が Node 互換寄りになっている
- Cloudflare Pages Functions 側で完全に Workers ネイティブを守るコストが高い
- alias による回避は保守性が低い
- 実績のある構成に揃える方が、今後のプロダクト展開で安全
特に、社内で複数プロダクトを並行で育てる場合は、「同じ認証基盤なら実行環境も揃える」方が保守性は高くなります。
今後の標準方針
今後、Cloudflare Pages + Better Auth を採用する新規プロダクトでは、まず次を標準候補として考えるのがよさそうです。
compatibility_flags = ["nodejs_compat"]
そのうえで、次のようなルールを持っておくと判断しやすくなります。
- Better Auth を使うなら、まず
nodejs_compat前提で検証する - どうしても Workers ネイティブで行く明確な理由があるときだけ、Node 互換なし構成を選ぶ
- その場合は、認証ライブラリ内部の Node 依存まで含めて事前検証する
- 既存プロジェクトで実績のある構成に揃えることを優先する
今回の教訓
1. 理想的な構成と、実際に保守しやすい構成は違う
Workers ネイティブに寄せる方針自体は悪くありません。ただし、依存ライブラリがその前提で動かないなら、設計思想より運用現実を優先した方がよいです。
2. 同じ技術スタックでも、wrangler.toml の差で結果は大きく変わる
Cloudflare Pages Functions では、compatibility_flags の違いだけでも認証の成否が変わります。認証まわりはコードだけでなく、ランタイム設定まで含めて比較する必要があります。
3. ブラウザ Console だけでは足りない
今回のような認証エラーは、ブラウザの画面上では「失敗しました」としか見えません。実際には wrangler pages dev 側のログに本当の原因が出ていました。
Cloudflare Pages Functions を使う場合は、次の3点をセットで見るべきです。
- ブラウザ Console
- Network タブ
wranglerのターミナルログ
まとめ
Cloudflare Pages Functions で Better Auth を使う場合、理想だけで Workers ネイティブ構成を選ぶと、ライブラリ内部の Node 依存で足を取られることがあります。
今回の結論は明確です。
- Better Auth を使うなら、まず
nodejs_compatを前提に考える - Cloudflare Pages Functions の auth 500 は、
node:async_hooksのような内部依存も疑う - Workers ネイティブを選ぶのは、理由と検証手順を持てるときだけにする
Cloudflare Pages、Better Auth、nodejs_compat の組み合わせで迷ったときは、まず「このライブラリは本当に Workers ネイティブで安定運用できるか」を疑うのが近道です。