cloudflare pagesにnext.js & auth.jsの環境を作ってみた

この記事は[Hacobu Advent Calendar 2024](https://qiita.com/advent-calendar/2024/hacobu)の22日目の記事です。

vercelの移行先候補としてよくあがるのが cloudflare pages。

そんなcloudflare pagesに[next.js](https://nextjs.org/)と[auth.js](https://authjs.dev/)のスタックで環境を作れないか試してみました。

cloudflare pages & next.jsはすでに詳しく解説されている記事があるので、割愛しつつハマりポイントのみコメントします。ここではauth.js周りの実装例を見ていきたいと思います。

### node compatibilityは大事

wranglerの設定ファイルである `wrangler.toml` を[ドキュメント](https://developers.cloudflare.com/workers/wrangler/configuration/)を参考に作成します。

```
#:schema node_modules/wrangler/config-schema.json
name = "my-next-app"
compatibility_date = "2024-12-16"
compatibility_flags = ["nodejs_compat"]
pages_build_output_dir = ".vercel/output/static"
```

nodeのapiを利用するためworker ランタイムで互換性フラグを有効にします。compatibility_dateは最新のものを選んでください。少し古いものだと、エラーが発生し動かなくなるケースがあります。

`wrangler.toml` からbindingsや環境変数の設定を読み取って型情報を生成することができます。

```bash
pnpm wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts
```

生成されたものを`tsconfig.json`のincludeに指定することで、`CloudflareEnv`という型が利用できます。

`wrangler.toml`はcommitするので機密情報は別で管理したいです。localで`.env`のようにsecretを読み込めるようにdatabaseのconnectionやgoogleなどの情報を`.dev.vars`に登録します。`.dev.vars`に登録することでwranglerがsecret情報として扱ってくれ型定義にも反映してくれます。

```
TURSO_URL=""
TURSO_AUTH_TOKEN=""

AUTH_SECRET=""
AUTH_GOOGLE_ID=""
AUTH_GOOGLE_SECRET=""
```

### [@cloudflare/next-on-pages](https://github.com/cloudflare/next-on-pages/tree/main/packages/next-on-pages#cloudflarenext-on-pages)を追加する

素のnext.jsをbuildしたものをcloudflare pagesにdeployするだけではもちろん動きません。cloudflare pageで動くようにbuildしたものを変換する必要があります。また開発時にもwranglerを起動し環境変数やbindingsをlocalで再現する必要があります。

それを実現してくれるツールが`@cloudflare/next-on-pages`です。

```bash
pnpm i @cloudflare/next-on-pages
```

開発時にlocalでbindingを読み取れるようにします。

```tsx
// next.config.ts
import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';

if (process.env.NODE_ENV === 'development') {
  await setupDevPlatform();
}

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

```

package.jsonのscriptにbuildコマンドを追加します

```bash
"pages:build": "npx @cloudflare/next-on-pages",
```

これで `pnpm pages:buld` でbuildしたものをcloudflare pagesにdeployできるようになりました。

## auth.js

基本的にはこちらの[ドキュメント](https://authjs.dev/getting-started/installation?framework=Next.js)通りに進めます。

下記コマンドで生成したランダムな文字列はsecretの`AUTH_SECRET`に指定してください。

```bash
pnpm dlx auth secret
```

auth.jsはデフォルトでsessionをCookieに保存します。なのでデータベースは必要ないのですが、今回はユーザー情報を保存する想定でデータベースを用意します。データベースはlibsqlのturso、ormはdrizzleを利用します。

こちらもドキュメントに詳しいやり方が書いてるのでそれに従って設定します。

- https://orm.drizzle.team/docs/get-started/turso-new
- https://authjs.dev/getting-started/adapters/drizzle

google認証をできるようにするため google providerを追加します。

ドキュメント通りに進めると最終的にauth.tsはこのようになります。

```tsx
// auth.ts
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "./schema"
 
export const { handlers, auth } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [Google],
})
```

ここまではほぼドキュメント通りに進めるだけです。node runtimeの場合はこれで完了なのですが、cloudflare pagesで動かすためにはもうひと手間加えます。というのも、workerでは `process.env` から環境変数が取得できません。auth.jsもdrizzleに　`getRequestContext`からenvを取得し明示的に渡してあげる必要があります。

```tsx
// auth.ts
import {getRequestContext} from '@cloudflare/next-on-pages';
import {drizzle} from 'drizzle-orm/libsql/web';
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { schema } from "./schema"
 
export const { handlers, auth } = NextAuth(() => {
  const {env} = getRequestContext();
  const db = drizzle({url: env.TURSO_URL, authToken: env.TURSO_TOKEN}, schema);
  return {
    secret: env.AUTH_SECRET,
    adapter: DrizzleAdapter(db),
    providers: [
      Google({
        clientId: env.AUTH_GOOGLE_ID,
        clientSecret: env.AUTH_GOOGLE_SECRET,
      }),
    ],
  };
})
```

最後に edge上で動くのでroute.tsにruntimeの宣言を追加します。

```tsx
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"

export const runtime = 'edge';

export const { GET, POST } = handlers
```

これで動くようになりました🎉

![Dec-18-2024 14-43-23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168353/2b4e4bc4-ec6c-38fa-9b64-6d29c8834eb7.gif)


## まとめ

簡単そうに見えるのですが、最初に試したときはnode compatibilityに対応していないapiをauth.jsが使っていたりしてエラーになるケースに何度もぶつかりました。またエラーの原因を特定するのも難しく対応されるまで待つ必要がありました。なので、本番環境で安定して動かすには少しハードルが高い気がします。とはいえ、コミュニティは活発なので放置されない安心感はあります。

コストも抑えられCDN上で動くというメリットを考えると、簡単なアプリを作る有力な選択肢にしたいと思いました。