🤝

Cloudflare PagesでPrisma(NextAuth, Supabase & Next.js API Routes)

2023/06/26に公開

シャローム、にわとろです。結婚指輪を買ったらいよいよお金がなくなってきたので、ECサービスでひと稼ぎしようと思い立ちました。慣れないデータベースもやるか、ついでに脱Vercelもやってやろう、とCloudflare Pagesを使い始めたらとんでもないエラーに続くエラーに巻き込まれたので、その話をしようと思います。

結論

先に結論から話しましょう。次のドキュメントで説明されている手順に従ってください。これで出てくるエラーは全て解決可能なので、頑張ってください。俺は頑張りました。
https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-cloudflare-workers

たのしい、ぎじゅつせんてい

ECとは非常に複雑なサービスです。慎重に技術を選んでやらねばなりません。決済にはStripe、フロントエンドには日本語よりも馴染んだNext.js 13を使うところまではサクサク決まりました。Stripeでも商品登録はできるようですが、もう少し複雑なこともしたかったので、最近使い始めたSupabaseを立ててデータを保存します。認証機能も付けましょうか。NextAuthにしましょう。

Prisma?

試しにNextAuthで認証を実装してCloudflare Pagesにデプロイしました。エラーが起きました。原因はCloudflare PagesがEdge Runtimeを使わせるせいです。そしてNextAuthはEdge Runtimeに対応してません。マジかよ。

Githubで探し回ったら開発チームがEdge Runtimeにも対応した実験版を作っていたのでありがたく使わせてもらうことにします。これは昨日記事にしました。
https://zenn.dev/niwatoro/articles/b87071718ac836

NextAuthとSupabaseを連携させようとしたら、いたるところにSQLとかいうたわごとが顔を出してきて困ります。俺はSQLなんて触ったことがないんだ。ちょうどNextAuth(の実験版)がデータベース連携を実装していたので、パラパラとドキュメントをめくっていたらPrismaを見つけました。SQLなしでよしなに連携してくれるらしいです。おーけい、キミに決めた。
https://authjs.dev/reference/adapter/prisma

結局次のような技術構成になりました。

ホスト フロントエンド データベース ORM 認証
Cloudflare Pages Next.js 13 Supabase Prisma NextAuth

これからが地獄の始まりです。

「Cloudflare PagesではPrismaは全く動きません」(2時間前)

まずは認証とユーザーデータの保存の連携から始めます。既に書いたNextAuthのAPIを拡張することになります。そもそもNextAuthが実験版なのでろくにドキュメントやら例やら型定義がありません。コードを参照しながら四苦八苦して実装します。とりあえずこんな感じで型エラーが消えました。

/src/app/api/[...nextauth]/route.ts
import NextAuth, { NextAuthConfig } from "next-auth";
import Google from "@auth/core/providers/google";
import { Provider } from "@auth/core/providers";
import { prisma } from "@/app/lib/prisma";
import { PrismaAdapter } from "@auth/prisma-adapter";

if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
  throw new Error("Missing Google OAuth credentials");
}

if (!process.env.NEXTAUTH_SECRET) {
  throw new Error("Missing NEXTAUTH_SECRET");
}

const authOptions: NextAuthConfig = {
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }) as Provider,
  ],
};

const handler = NextAuth(authOptions);
export const GET = handler.handlers.GET;
export const POST = handler.handlers.POST;

export const runtime = "edge";
/src/app/lib/prisma.ts
import { PrismaClient } from "@prisma/client";

export const prisma = new PrismaClient();

実行するとさっそくエラーが表示されます。まあ慣れたものです。

Error: PrismaClient is unable to be run in the browser.

Next.jsを触ってる人ならピンとくるエラーですね。クライアントとサーバーはちゃんと分けとけ、というやつです。でもなあ、これAPI Routesだよなあ、そもそも要るんだっけと思いながら冒頭に"use server";を付けます。

x Only async functions are allowed to be exported in a "use server" file.
,-[/src/app/api/auth/[...nextauth]/route.ts:31:1]
31 | export const GET = handler.handlers.GET;
32 | export const POST = handler.handlers.POST;
33 |
34 | export const runtime = "edge";
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`----

Cloudflare Pagesのために付けてあげたexport const runtime = "edge";がエラーを起こしたみたいです。ただこれを削除してしまうとCloudflare Pagesが使えないので、仕方なくこの方法はやめておくことにしました。こんなときは、インターネットに頼りましょう。

色々調べてみるとけっこう同じエラーに遭遇した人がいるみたいです。ほんほん、とうなりながらまだ調べていくと、あるイシューに行き当たりました。
https://github.com/prisma/prisma/issues/19500

大意としては、PrismaはEdge Runtimeと互換性がないらしいです。またお前か。そして最新のコメント。なんと投稿されたのはたった2時間前。

この問題のために、Cloudflare PagesにデプロイされたNext.jsのプロジェクトは、Prismaと全く互換性がないようです。

おい!!!!!!
2時間前に報告されてるならまだアプデもないだろうし、これもうダメだろ!

俺は絶望のあまり手がわなわなと震わせて、手元のカップから水を零しました。絶望ついでにTwitterに投稿します。

https://twitter.com/niwatoro277/status/1672888782209810432

するとさっそく大学の先輩から反応をもらいました。何でもPrismaはCloudflare Pagesと相性が悪いらしく、別のORMを使うといいみたいです。俺でも知ってる大エンジニア、mizchiさんが書いた記事も教えてもらいました。
https://zenn.dev/mizchi/articles/d1-drizzle-orm

データベースには今アツいD1を、それに伴ってORMにdrizzle-ormを使うといけるそうです。ふむ。

見えた一筋の光

現在の技術構成は、Cloudflare Pages x Next.js 13 x Supabase x Prisma x NextAuthです。もしmizchiさんの記事通りにするなら、SupabaseとPrismaの代わりに初めて触るD1とdrizzle-ormを使うだけでなく、連携がないNextAuthも諦めなくてはなりません。

実はD1とNextAuthの連携機能自体は開発されているらしいです。ただ、12月には開発者がイシュー開いていて、Cloudflareもすぐにプルリクがマージされたらドキュメント追加しておくよと返信しているのに、ここ3ヶ月間何も音沙汰がありません。たぶん、当分連携はできないでしょう。
https://github.com/nextauthjs/next-auth/issues/5918

さすがに知らない技術/フレームワークも3つも入れるのはキツい。Vercelを使えばClooudflare Pagesをやめるだけで済むかもしれないが、わざわざVercelを抜くために始めたのにここで戻るのはダサすぎる。なので、技術構成はこのままにCloudflare Pagesへデプロイすることを目指します。互換性がないと言われていたPrismaClientの中身をちょこちょこいじっていると気になるコードに行き当たりました。

Edge対応PrismaClientの発見

/src/app/lib/prisma.ts
+ import { PrismaClient } from "@prisma/client/edge";
- import { PrismaClient } from "@prisma/client";

 const client = new PrismaClient();

お?

どうやら@prisma/clientには色んなランタイム専用のクライアントが定義されているみたいです。何だEdge対応してるじゃんと喜び勇んでnpm run devをするとあえなくエラー。

TypeError: Cannot read property findUnique

import文しか換えてないので、エラーの原因はライブラリ自体にありそうです。中身を見てみます。

/node_modules/@prisma/client/edge.js
module.exports = {
  ...require('.prisma/client/edge'),
}

じゃあこのrequire先を見てみましょう……と開いてみると、クソ長かったのでとりあえず忘れることにします。ここで考え方を変えて、ファイルではなくフォルダを見ます。/node_modules/.prismaですが、これはどんなフォルダでしたっけ?

Prisma Data Proxyの設定

.prismanpx prisma generate/prisma/schema.prismaから生成されるフォルダです。とすると、ここに問題があるとすれば参照側のNextAuthの書き間違いではなくてPrismaの設定にありそうです。といっても設定した覚えのあるPrismaの項目なんてそれこそnpx prisma generateしかないので、一旦Prismaのドキュメントを読み返します。

すると、冒頭で紹介したタイトルそのままのドキュメントに行き当たりました。
https://www.prisma.io/docs/guides/deployment/deployment-guides/deploying-to-cloudflare-workers

なんとPrismaが公式でCloudflare Workersをサポートしています。2時間前のあのコメントは一体何だったんでしょう。WorkersとPagesって何が違うんだとか、この飛び交ってるWranglerって何なんだとか疑問は尽きませんか、全部無視してPrisma Data Platformに飛んで設定を進めます。
https://cloud.prisma.io/projects
始めにGithubと連携させます。次はスクリーンショットのようにポチポチ記入していきます。まずはSupabaseのコンソールからProject Settings > Database > Connection string > Nodejsで接続文字列をペーストします。ここで接続文字列中の[YOUR-PASSWORD]はSupabaseのプロジェクトを作る時に入力したパスワードで置換しておきます。

その後、Static IPsをEnableして、表示されたIPアドレスをコピーします。設定が終わるとPrismaのデータベースURLが表示されるのでそれもメモしておきましょう。

さてさっき表示されたPrismaのIPアドレス以外からの接続を制限します。まずSupabaseのコンソールからProject Settings > Database > Network RestrictionsでRestrict all accessで全てのアクセスを拒否します。

次はPrismaからの接続を例外的に許可しなければならないのですが、この操作は2023年6月25日現在ダッシュボードからはできないので以下ターミナルから操作します。

Supabase CLIをインストール。

npm i supabase

Supabaseにログイン。ダッシュボードに表示されたURLから飛んでアクセストークンを生成し、ターミナルに貼り付けます。

npx supabase login

まず全てのIPアドレスからの接続を拒否していることを確認します。ここで{ref}には、Supabaseのダッシュボードからプロジェクトを選択したときに、URLに現れる英数字列を入れましょう。それで上手くいきました。

npx supabase network-restrictions --project-ref {ref} get --experimental

次にPrismaのIPアドレスからの接続を例外的に許可します。{ref}は同じく、{address}には一つ一つPrismaのIPアドレスを入れていきましょう。2つで足りなければ、適宜--db-allow-cidr {address}/0を追加していきます。なお、IPアドレスの後ろの/に続く数字はCIDRブロックの大きさを表すらしいですが、よく知らないので0にしておきました。今のところ不都合は生じていません。

npx supabase network-restrictions --project-ref {ref} update --db-allow-cidr {address1}/0 --db-allow-cidr {address2}/0 --experimental

例外設定が終わったらSupabaseのダッシュボードに戻ってきちんとIPアドレスが追加されていることを確認しましょう。ダッシュボードでは追加はできませんが確認はできます。

まだまだ終わりません。次はNext.jsでPrismaを設定していきます。まずルート直下の.envを開きましょう。DATABASE_URLにはPrisma Data Platformでの設定の最後に表示されたURLを、DIRECT_URLには設定の一番最初にペーストした接続文字列を入れます。大体の形は書いておくので、間違った文字列を入れていないか確認しましょう。

.env
DATABASE_URL="prisma://aws-us-east-1.prisma-data.com/?api_key=XXXXXXXXXX"
DIRECT_URL="postgres://postgres:[パスワード]@db.xxxxxxxxxx.supabase.co:0000/postgress"

最後に/prisma/schema.prismaを設定します。datasourcedirectUrlを追加します。

/prisma/schema.prisma
 datasource db {
   provider  = "postgresql"
   url       = env("DATABASE_URL")
+  directUrl = env("DIRECT_URL")
 }

ここまで終えるとようやく/.prismaフォルダを再生成できます。Next.jsプロジェクトのルートでコマンドを実行します。

npx prisma generate --data-proxy

NextAuthとPrismaの連携設定

これでようやくCloudflare Pagesにデプロイできるかとウキウキしてnpm run devすると、またしてもエラーになります。そろそろ嫌になってくるぜ。

The table public.Session does not exist in the current database.

しかしここまでの艱難辛苦を乗り越えてきた俺にはお茶の子さいさいです。データベースにSessionテーブルがないと言ってるんですが、設定してないのである訳がありません。Prismaが必要な設定なら勝手に加えてくれるはずなので、NextAuthが要求してるんでしょう。NextAuthのドキュメントに戻れば、ビンゴ、ちゃんとあります。

https://authjs.dev/reference/adapter/prisma

/prisma/schema.prisma
model Account {
  id                 String  @id @default(cuid())
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.Text
  access_token       String?  @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.Text
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

以上のテーブルの定義を/prisma/schema.prismaに加えました。Userテーブルは俺が定義していたものもあるので、上手く融合させておきます。

これをPrismaとその向こうのSupabaseに反映し終わったら、いよいよCloudflare Pagesにデプロイできるはずです。気張っていきましょう。

npx prisma generate
npx prisma migrate dev

おっとまだエラーが出ます。

prepared statement "s0" already exists

検索したら溝口さんの記事がヒットしたので、言われたとおりにDIRECT_URLの末尾に?schema=public&pgbouncer=trueを付け足してもう一度npx prisma migrate devを実行します。
https://zenn.dev/coji/scraps/d30e55b38cc707

最後にCloudflare Pagesの設定で環境変数を全て入れてあげれば、ちゃあんとデプロイされてくれました。やった!!!!!

辞世の一句

エラー死すべし 我も逝こうぞ エンジニアゆえ

過去の記事

ここまで読んでくれてお疲れ様でした! 昔書いた記事もあるのでデザートによろしければ。
https://zenn.dev/niwatoro/articles/180f6185c382bb
https://zenn.dev/niwatoro/articles/51f22ab69e0c9b

Discussion