Cloudflare PagesでPrisma(NextAuth, Supabase & Next.js API Routes)
シャローム、にわとろです。結婚指輪を買ったらいよいよお金がなくなってきたので、ECサービスでひと稼ぎしようと思い立ちました。慣れないデータベースもやるか、ついでに脱Vercelもやってやろう、とCloudflare Pagesを使い始めたらとんでもないエラーに続くエラーに巻き込まれたので、その話をしようと思います。
結論
先に結論から話しましょう。次のドキュメントで説明されている手順に従ってください。これで出てくるエラーは全て解決可能なので、頑張ってください。俺は頑張りました。
たのしい、ぎじゅつせんてい
ECとは非常に複雑なサービスです。慎重に技術を選んでやらねばなりません。決済にはStripe、フロントエンドには日本語よりも馴染んだNext.js 13を使うところまではサクサク決まりました。Stripeでも商品登録はできるようですが、もう少し複雑なこともしたかったので、最近使い始めたSupabaseを立ててデータを保存します。認証機能も付けましょうか。NextAuthにしましょう。
Prisma?
試しにNextAuthで認証を実装してCloudflare Pagesにデプロイしました。エラーが起きました。原因はCloudflare PagesがEdge Runtimeを使わせるせいです。そしてNextAuthはEdge Runtimeに対応してません。マジかよ。
Githubで探し回ったら開発チームがEdge Runtimeにも対応した実験版を作っていたのでありがたく使わせてもらうことにします。これは昨日記事にしました。
NextAuthとSupabaseを連携させようとしたら、いたるところにSQLとかいうたわごとが顔を出してきて困ります。俺はSQLなんて触ったことがないんだ。ちょうどNextAuth(の実験版)がデータベース連携を実装していたので、パラパラとドキュメントをめくっていたらPrismaを見つけました。SQLなしでよしなに連携してくれるらしいです。おーけい、キミに決めた。
結局次のような技術構成になりました。
ホスト | フロントエンド | データベース | ORM | 認証 |
---|---|---|---|---|
Cloudflare Pages | Next.js 13 | Supabase | Prisma | NextAuth |
これからが地獄の始まりです。
「Cloudflare PagesではPrismaは全く動きません」(2時間前)
まずは認証とユーザーデータの保存の連携から始めます。既に書いたNextAuthのAPIを拡張することになります。そもそもNextAuthが実験版なのでろくにドキュメントやら例やら型定義がありません。コードを参照しながら四苦八苦して実装します。とりあえずこんな感じで型エラーが消えました。
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";
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が使えないので、仕方なくこの方法はやめておくことにしました。こんなときは、インターネットに頼りましょう。
色々調べてみるとけっこう同じエラーに遭遇した人がいるみたいです。ほんほん、とうなりながらまだ調べていくと、あるイシューに行き当たりました。
大意としては、PrismaはEdge Runtimeと互換性がないらしいです。またお前か。そして最新のコメント。なんと投稿されたのはたった2時間前。
この問題のために、Cloudflare PagesにデプロイされたNext.jsのプロジェクトは、Prismaと全く互換性がないようです。
おい!!!!!!
2時間前に報告されてるならまだアプデもないだろうし、これもうダメだろ!
俺は絶望のあまり手がわなわなと震わせて、手元のカップから水を零しました。絶望ついでにTwitterに投稿します。
するとさっそく大学の先輩から反応をもらいました。何でもPrismaはCloudflare Pagesと相性が悪いらしく、別のORMを使うといいみたいです。俺でも知ってる大エンジニア、mizchiさんが書いた記事も教えてもらいました。
データベースには今アツい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ヶ月間何も音沙汰がありません。たぶん、当分連携はできないでしょう。
さすがに知らない技術/フレームワークも3つも入れるのはキツい。Vercelを使えばClooudflare Pagesをやめるだけで済むかもしれないが、わざわざVercelを抜くために始めたのにここで戻るのはダサすぎる。なので、技術構成はこのままにCloudflare Pagesへデプロイすることを目指します。互換性がないと言われていたPrismaClient
の中身をちょこちょこいじっていると気になるコードに行き当たりました。
Edge対応PrismaClientの発見
+ 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文しか換えてないので、エラーの原因はライブラリ自体にありそうです。中身を見てみます。
module.exports = {
...require('.prisma/client/edge'),
}
じゃあこのrequire先を見てみましょう……と開いてみると、クソ長かったのでとりあえず忘れることにします。ここで考え方を変えて、ファイルではなくフォルダを見ます。/node_modules/.prisma
ですが、これはどんなフォルダでしたっけ?
Prisma Data Proxyの設定
.prisma
はnpx prisma generate
で/prisma/schema.prisma
から生成されるフォルダです。とすると、ここに問題があるとすれば参照側のNextAuthの書き間違いではなくてPrismaの設定にありそうです。といっても設定した覚えのあるPrismaの項目なんてそれこそnpx prisma generate
しかないので、一旦Prismaのドキュメントを読み返します。
すると、冒頭で紹介したタイトルそのままのドキュメントに行き当たりました。
なんとPrismaが公式でCloudflare Workersをサポートしています。2時間前のあのコメントは一体何だったんでしょう。WorkersとPagesって何が違うんだとか、この飛び交ってるWranglerって何なんだとか疑問は尽きませんか、全部無視してPrisma Data Platformに飛んで設定を進めます。
始めに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には設定の一番最初にペーストした接続文字列を入れます。大体の形は書いておくので、間違った文字列を入れていないか確認しましょう。
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
を設定します。datasource
にdirectUrl
を追加します。
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のドキュメントに戻れば、ビンゴ、ちゃんとあります。
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
を実行します。
最後にCloudflare Pagesの設定で環境変数を全て入れてあげれば、ちゃあんとデプロイされてくれました。やった!!!!!
辞世の一句
エラー死すべし 我も逝こうぞ エンジニアゆえ
過去の記事
ここまで読んでくれてお疲れ様でした! 昔書いた記事もあるのでデザートによろしければ。
Discussion