Skip to content
Go back

Cloudflare, Nextjs, Authjs, Prisma, D1

Updated:  at  04:57 PM

⚠️注意⚠️: 絶望的な欠点

バンドルサイズが3.6MBと、3MB以上になるため、フリープランでは使えません。

Total Upload: 14251.07 KiB / gzip: 3601.99 KiB

TL;DR

プロジェクトを一から作りたくない場合は、以下のリポジトリをクローンして、READMEに従えば、すぐに開発を始められます。

https://github.com/torimapowered/cloudflare-fullstack


Sources

ベースプロジェクトの作成

cloudflare-fullstackの部分を任意のプロジェクト名に置き換えてください。

npm create cloudflare@latest -- cloudflare-fullstack --framework=next

選択肢はすべてデフォルトでok

✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a src/ directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for next dev? … No / Yes
✔ Would you like to customize the import alias (@/* by default)? … No / Yes

compilerOptions.targetの変更

tsconfig.jsoncompilerOptions.targetを以下のようにES2022に変更。

top level awaitを使うために必要。

{
  "compilerOptions": {
    "target": "ES2022",
  }
}

next.config.ts

const nextConfig: NextConfig = {
  serverExternalPackages: ["@prisma/client", ".prisma/client"],
};

を追加。

Auth.jsで使う認証情報

.dev.varに以下のように追記する

AUTH_SECRET = "<replace-me>"
AUTH_RESEND_KEY = "<replace-me>"
AUTH_EMAIL_FROM = "[email protected]"
AUTH_URL = "http://localhost:8787/"

auth.jsのインストール

npm i next-auth@beta

AUTH_SECRETの作成

npx auth secret

.env.localAUTH_SECRETが作成されるので、

npx wrangler secret put AUTH_SECRET

で値をアップロードする。

そうしたら、もう一度

npx auth secret

を実行してAUTH_SECRETを再生成する。

? Overwrite existing `AUTH_SECRET`? › (y/N)

と聞かれるのでy.env.localを上書き。

そして生成された新しいAUTH_SECRET.dev.varsに追記する。

.env.localは消してもよい。

D1のデータベース作成

npx wrangler d1 create cloudflare-fullstack

成功すると

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "cloudflare-fullstack",
      "database_id": "f4d98234-e618-47c9-9c75-300c2d48deb5"
    }
  ]
}

のような出力がされるので、wrangler.jsoncに貼り付ける。 既存の$schema, name, mainなどと同じ層にd1_databasesが来るようにする。

/**
* Bindings
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
* databases, object storage, AI inference, real-time communication and more.
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/

というコメント説明があるので、その下に貼り付けるとよいでしょう。

resend.comの設定

npx wrangler secret put AUTH_EMAIL_FROM

[email protected]と設定します。

.dev.varsにも同じく設定します。

npx wrangler secret put AUTH_RESEND_KEY

で、resend.comで取得したAPIキーを設定します。

また、.dev.varsAUTH_RESEND_KEYにも同様にAPIキーを設定しますが、 セキュリティのために、先ほどCloudflareにアップロードしたものとは別のキーを生成して使いましょう。

cloudflare-env.d.tsの自動生成

npm run cf-typegen

cloudflare-env.d.tsを自動的に生成します。

これにより、これまで.dev.varswrangler.jsoncに設定したものを、typescriptで使えるようになります。

Prisma

npm i -D prisma
npm install @prisma/adapter-d1 @prisma/client

でインストール。

npx prisma init --datasource-provider sqlite

.gitignore.envを忘れず追加しておきます。

schema.prisma

D1Adapterなどのadapterを使えるようにするために、app/prisma/schema.prismaclientpreviewFeatures = ["driverAdapters"]を追記します。

以下のようになります。

generator client {
  provider = "prisma-client-js"
  output   = "../app/generated/prisma"
  previewFeatures = ["driverAdapters"]
}

outputを削除する

schema.prismaには以下のようにoutput行が自動的に生成されているはずです。

generator client {
  provider = "prisma-client-js"
  output   = "../app/generated/prisma"
}

通常のprismaではこれがデフォルトであり推奨されていますが、 今回のようにOpenNEXTで使う場合、outputを指定してはいけません(行を削除する必要があります)。

Auth.jsのスキーマを追加

app/prisma/schema.prismaに、Auth.jsが使用するスキーマ を以下のように追記します

model User {
  id            String          @id @default(cuid())
  name          String?
  email         String?         @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  // Optional for WebAuthn support
  Authenticator Authenticator[]
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  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)
 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
 
model VerificationToken {
  identifier String
  token      String
  expires    DateTime
 
  @@unique([identifier, token])
}
 
// Optional for WebAuthn support
model Authenticator {
  credentialID         String  @unique
  userId               String
  providerAccountId    String
  credentialPublicKey  String
  counter              Int
  credentialDeviceType String
  credentialBackedUp   Boolean
  transports           String?
 
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@id([userId, credentialID])
}

Cloudflareの接続情報をPrismaに渡す

.envに以下を追記します。

DATABASE_URLは既存のもの(file:./dev.db)から置き換えてください。

DATABASE_URL="file:../.wrangler/state/v3/d1/miniflare-D1DatabaseObject/74121e82aac5ba8d638903f7126ebe2f3ce1557d5b8624e7992d5e63599314d0.sqlite"

CLOUDFLARE_ACCOUNT_ID="0773..."
CLOUDFLARE_DATABASE_ID="01f30366-..."
CLOUDFLARE_D1_TOKEN="F8Cg..."

.wrangler/state/v3/d1/miniflare-D1DatabaseObject/74121e82aac5ba8d638903f7126ebe2f3ce1557d5b8624e7992d5e63599314d0.sqliteのようなファイルを探してください。 末尾がsqlite-shm, sqlite-walのものがあるかもしれませんが、今は関係ありません。

重要なのはファイル名の乱数で、それが1種類しかない場合はそれを使います。 2種類以上ある場合でも、この乱数はwrangler.jsoncにコピペしたdatabase_idのハッシュなので、 自作の計算機 に入れると計算できます。

.wrangler/state/v3/d1/miniflare-D1DatabaseObjectディレクトリやファイルが見つからない場合はnpx wrangler d1 execute cloudflare-fullstack --command "select 1;" のような適当なSQLを実行すると生成されます。

これらの環境変数はNextjsのアプリから使われるわけではなく、開発者がprisma migrate devなどを実行したときに使われるため、 .dev.varsではなく.envに置いています。

remote.prisma.config.ts

remote.prisma.config.tsを以下の内容で作成します。

import path from 'node:path'
import type { PrismaConfig } from 'prisma'
import { PrismaD1HTTP } from '@prisma/adapter-d1'

// import your .env file
import 'dotenv/config'

type Env = {
  CLOUDFLARE_D1_TOKEN: string
  CLOUDFLARE_ACCOUNT_ID: string
  CLOUDFLARE_DATABASE_ID: string
}

export default {
  earlyAccess: true,
  schema: path.join('prisma', 'schema.prisma'),
  migrate: {
    async adapter(env) {
      return new PrismaD1HTTP({
        CLOUDFLARE_D1_TOKEN: env.CLOUDFLARE_D1_TOKEN,
        CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID,
        CLOUDFLARE_DATABASE_ID: env.CLOUDFLARE_DATABASE_ID,
      })
    },
  },
} satisfies PrismaConfig<Env>

Prisma公式から少し改変し、 prisma migration devとそれ以外の場合で(簡易的に)挙動を変えています。

prisma.config.tsに作成すると、prisma migrate devであろうとprisma migrate deployであろうと自動的に読み込まれてしまいます。 するとprisma migration devのときでも本番のD1に影響を与えてしまいます。 そのため、自動的に読み込まれないようファイル名をremote.prisma.config.tsに変えて、prisma migrate deploy --config ./remote.prisma.config.tsのように デプロイ時にだけ読み込むようにします。

package.jsonscriptsに以下を追加しておきましょう。

{
  "db:migrate:dev": "prisma migrate dev",
  "db:migrate:deploy": "prisma migrate deploy --config ./remote.prisma.config.ts"
}

prisma clientの作成

npx prisma generate

でprisma clientを生成します。

vscodeの場合、後述のPrismaClientanyのままである場合があるため、 Ctrl + Shift + P -> TypeScript: Restart TS Server を実行しておきましょう。

app/auth.ts

npm install @auth/prisma-adapter

でインストール。

lib/db.tsを以下のように作成します。

import { getCloudflareContext } from "@opennextjs/cloudflare";
// You can use cache from react to cache the client during the same request
// this is not mandatory and only has an effect for server components
import { cache } from "react";
import { PrismaClient } from "@prisma/client";
import { PrismaD1 } from "@prisma/adapter-d1";
 
export const getDb = cache(() => {
  const { env } = getCloudflareContext();
  const adapter = new PrismaD1(env.MY_D1);
  return new PrismaClient({ adapter });
});
 
// If you need access to `getCloudflareContext` in a static route (i.e. ISR/SSG), you should use the async version of `getCloudflareContext` to get the context.
export const getDbAsync = async () => {
  const { env } = await getCloudflareContext({ async: true });
  const adapter = new PrismaD1(env.MY_D1);
  const prisma = new PrismaClient({ adapter });
  return prisma;
};

app/auth.tsを以下のように作成します。

import NextAuth from "next-auth";
import { NextAuthResult } from "next-auth";
import Resend from "next-auth/providers/resend";
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { getDbAsync } from "@/lib/db";

const authResult = async (): Promise<NextAuthResult> => {
  const prisma = await getDbAsync();
  return NextAuth({
    providers: [
      Resend({
        apiKey: (await getCloudflareContext({ async: true })).env
          .AUTH_RESEND_KEY,
        from: (await getCloudflareContext({ async: true })).env.AUTH_EMAIL_FROM,
      }),
    ],
    adapter: PrismaAdapter(prisma)
  });
};

export const { handlers, signIn, signOut, auth } = await authResult();

このapp/auth.tsは、

の公式ドキュメントのコードを組み合わせて(人間が)作成しました。

auth api

app/api/auth/[...nextauth]/route.tsを以下のように作成する。

import { handlers } from "../../../auth";

export const { GET, POST } = handlers;

middleware.tsを以下のように作成します。

export { auth as middleware } from "./app/auth";

prismaの初期化

いつもどおりに進めると、prisma migration devは成功しますが、その後のprisma migration deployで以下のようなエラーが出ます。

Error: P3005

The database schema is not empty. Read more about how to baseline an existing production database: https://pris.ly/d/migrate-baseline

D1には予約済みの_cf_KVというテーブルがあり、読み書きできません。

なのに確かにテーブルとして存在はしているので、prismaの初期化時のテーブル不存在チェックに引っかかってしまいます。

そのため、ダミーのmigrationファイルを作成し、「テーブルが存在するのはマイグレーションをしたからだ」と思わせることにします。

ダミーのmigration

prisma/migrations/0_init/migration.sqlを作成します。 0_initはなんでもいいです。

内容は

SELECT 1

とだけ書いておきます。これは何もしないSQLです。

npx prisma migrate resolve --config ./remote.prisma.config.ts --applied 0_init

でダミーのmigrationを適用します。

TableDoesNotExistエラーが発生しますが、これは初回migrationだから_prisma_migrationsというテーブルが見つからなかったという正常な動作です。 実行後に_prisma_migrationsが作成されます。

最終的にMigration 0_init marked as applied.のように表示されていれば問題ありません。

最初のマイグレーション

npx prisma migrate dev --name init

schema.prismaを開発用ローカルDBに反映させます。

その後、

npm run db:migrate:deploy

で本番のD1に反映させます。

お疲れさまでした!!!


Share this post on:

Previous Post
No module named 'aws_lambda_powertools'
Next Post
Gitで直前のコミットに戻すワンライナー