この記事のポイント
- Clerkのwebhookの使い方を学べる
- Clerkのwebhookの検証方法がわかる
- ngrokを使い、ローカル環境でも外部からアクセス可能なエンドポイントの作成方法がわかる
経緯
ClerkのOAuthを使用してユーザー認証を行っている場合、新規ユーザーの登録をWebアプリ側で検知するのが難しい。そのため、ClerkのWebhookを利用し、自動的にDBへユーザー情報を送る仕組みを構築しました。そのときの開発メモです。
前提
- 今回は ユーザーのサインアップ時(User creation) に、ClerkのWebhookを使って、DBにユーザー情報を自動で送るようにするための手順を構築する想定で進めます。
- 基本的にはClerkの公式ドキュメントの手順に沿って解説します。
- DBはNeon(postgresql)を使用しています。
実装手順
1. ngrokのセットアップ
今回はClerkの公式ドキュメントにもあるようにngrok
を使って、Webhookのリクエストをアプリが受け取れるようにします。
1-1. ngrokのインストールとセットアップ
ngrok公式ページにアクセスして、サインアップしてセットアップしてください。
※ログインするとSetup&Installationから手順を確認できます。
1-2. 外部公開URLを取得
ご自身の開発環境をnpm run dev
などで立ち上げ、そのローカルホストと同じものをngrokで実行します。
▽例: http://localhost:3000 の場合
ngrok http 3000
以下の赤枠のURL(Forwarding)が確認できればOKです。
補足:ngrokとは
ローカルで動作しているWebアプリを、一時的にインターネット上に公開できるツールです。今回で言うと、ClerkのWebhookからローカル環境へリクエストを送るために使用します。
2. ClerkのWebhookを設定する
2-1. Clerkのダッシュボード にアクセス
2-2. Webhooks タブからAdd Endpointボタンをクリック
2-3. POST
で受け取るエンドポイントを設定
①Endpoint URL
Forwarding URL + エンドポイント(例:/api/webhooks/user)となるようにして登録。
例:https://fawn-two-nominally.ngrok-free.app/api/webhooks/user
②Description
エンドポイントに関する説明を必要であれば追加。
(例:新規ユーザーがサインアップした場合、DBに自動でユーザーデータを送信)
③Subscribe to events
どのイベント時にこのエンドポイントにPOST
リクエストを送るかを設定。
今回はユーザー新規登録時なので、Webhookのイベントとしてuser.created
を選択。
3..env.local
ファイルにSigning Secret
を追加
Webhookのペイロードを検証するには、エンドポイントのSigning Secret
が必要です。
作成したエンドポイントの詳細画面からSigning Secret
を取得し、.env.local
ファイルに追加します。
.env.localファイルには、基本的に既にデータベースのURL情報やClerk API キーが含まれているはずなので、次のようになると思います。
▽.env.localファイル
DATABASE_URL=postgresql://database_url
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YXdha2Utb3gtODUuY2xlcmsuYWNjb3VudHMuZGV2JA
CLERK_SECRET_KEY=sk_test_5DbmoNJeli74tMRgtzZLBY4SC6gpTLbNGnvAPeWJkE
SIGNING_SECRET=whsec_123
4. ミドルウェアでwebhookのルートがパブリックになるように設定
clerkMiddleware()
を使用している場合は、/api/webhooks(.*)
ルートがパブリックに設定されていることを確認してください。ルートの設定については clerkMiddleware() のガイドを参照してください。
▽参考:以下のような設定であればapi
がパブリックで実行されるはずです
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
export const config = {
matcher: [
// Skip Next.js internals and all static files, unless found in search params
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}
5. svixをインストール
Clerkはsvixを使ってWebhookの署名付きリクエストを送っているので、サーバー側で不正なリクエストでないか検証するために使います。ターミナルで以下のコマンドを実行し、パッケージをインストールしてください。
npm install svix
6. Next.js API Route を作成する
2で設定したエンドポイントに合わせて、ルートファイルを作成し、Webhookリクエストを受け取れるようにする。
▽例:/src/app/api/webhooks/user/route.ts
import { NextResponse } from 'next/server';
import { Pool } from 'pg';
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
// Neon DB へのコネクションを設定
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // Add to .env
});
// POSTリクエスト
export async function POST(req: Request) {
const SIGNING_SECRET = process.env.SIGNING_SECRET;
if (!SIGNING_SECRET) {
throw new Error('Error: Please add SIGNING_SECRET from Clerk Dashboard to .env or .env.local');
}
// Create new Svix instance with secret
const wh = new Webhook(SIGNING_SECRET);
// Get headers
const headerPayload = await headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');
// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error: Missing Svix headers', {
status: 400,
});
}
// Get body
const payload = await req.json();
const body = JSON.stringify(payload);
let evt: WebhookEvent;
// Verify payload with headers
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent;
} catch (err) {
console.error('Error: Could not verify webhook:', err);
return new Response('Error: Verification error', {
status: 400,
});
}
// `user.created` イベントの処理(適宜変更してください)
try {
if (evt.type === 'user.created') {
const { id, email_addresses } = evt.data;
const email = email_addresses?.[0]?.email_address || "";
// DBにユーザーを登録
await pool.query(
"INSERT INTO users (id, email) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING",
[id, email]
);
console.log(`User ${id} added to database.`);
return NextResponse.json({ message: 'User saved to DB' }, { status: 200 });
}
return NextResponse.json({ message: 'Unhandled event' }, { status: 200 });
} catch (error) {
console.error('Webhook Error:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
7.Webhookの検証
- 全ての設定が完了したらエンドポイントの設定ページから「Testing」タブを選択
- Send eventのプルダウンから
user.created
を選択
- ページ下部のSend Exampleを選択
- Message Attemptsのセクションで実行したイベントが以下のように
Succeeded
と表示されていることができれば成功です!🎉
これで、ClerkのOAuthを使ったユーザー認証時でも、新規ユーザー情報をDBへ自動登録できるようになります!
実際にアプリ内でユーザーを新規登録してみて正しく実行されるか確認してください。
デバッグ
テストでエラーが発生した場合に、主に確認すべき点を挙げます。
- ミドルウェアの設定を確認(apiがパブリックで実行されるようになっているか)
- Clerkの設定画面で正しいエンドポイントが設定されているか
URL+/api/webhooks/指定のルート
となっているか確認
例:ルートファイル)/src/app /api/webhooks/user/route.ts
エンドポイント)http://〜 /api/webhooks/user - ルートハンドラやAPIルートの設定が正しいか確認
Webhookに問題がある場合、基本的なルートハンドラとレスポンスを作成し、ローカルでテストすることができます。
▽app/webhooks/test/route.ts
export async function POST() {
return Response.json({ message: 'The route is working' })
}
②アプリを実行
③以下のコマンドを実行
curl -H 'Content-Type: application/json' \
-X POST http://localhost:3000/api/webhooks/test
④{"message":"The route is working"}
が表示されたら、基本的なルートハンドラは動作しており、ビルドする準備ができています。
補足:Clerk上でユーザー情報を削除したい場合
デプロイする場合
基本的に手順は同じです。
①Clerkでデプロイ先のURL+エンドポイント(/api/webhooks/*)
を追加
②Signing Secret
をデプロイ先のEnvironment Variables
に設定
③デプロイ
これでローカル環境と同様に実行できるはずです。
まとめ
- Clerk の
Webhook
を使うと、新規ユーザー登録をリアルタイムで検知できる。 - ローカル開発時は
ngrok
を活用すると、外部からアクセス可能なエンドポイントを作れる。