Key Points
- Learn how to use Clerk webhooks.
- Understand how to verify Clerk webhooks.
- Use ngrok to expose a local endpoint for external access.
Background
When using Clerk OAuth for user authentication, detecting new user registrations from the web app can be challenging. To address this, we use Clerk webhooks to automatically send user data to a database upon signup. This article walks through the implementation steps.
Prerequisites
- Using Clerk webhooks to send user data to a database upon signup.
- The setup follows Clerk’s official documentation.
- The database used is Neon (PostgreSQL).
Implementation Steps
1. Set Up ngrok
Clerk’s documentation recommends using ngrok to allow webhook requests to reach the local app.
1.1 Install and Set Up ngrok
Sign up on ngrok's official website and follow the installation steps provided in the Setup & Installation section.
1.2 Obtain an External URL
Start your local development server (npm run dev
), then run
ngrok http [Port]
Example: In the case of http://localhost:3000
ngrok http 3000
Copy the Forwarding
URL displayed.
What is ngrok?
Ngrok allows a locally running web app to be temporarily accessible over the internet. In this case, it enables Clerk webhooks to send requests to a local development server.
2. Configure Clerk Webhooks
2.1 Access Clerk Dashboard
Go to the Clerk dashboard.
2.2 Add a Webhook Endpoint
- Navigate to the Webhooks tab and click Add Endpoint.
- Set the Endpoint URL to the ngrok forwarding URL +
/api/webhooks/user
(e.g.,https://example.ngrok-free.app/api/webhooks/user
). - Add a description if needed.
- Subscribe to the user.created event.
- Click Create.
3. Add Signing Secret to .env.local
On the endpoint's settings page, copy the Signing Secret from the webhook settings.
Add Signing Secret, like SIGNING_SECRET=whsec_123
, to your .env.local
file, which should already include your Clerk API keys and DB URL.
The file should resemble:
DATABASE_URL=postgresql://database_url
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YXdha2Utb3gtODUuY2xlcmsuYWNjb3VudHMuZGV2JA
CLERK_SECRET_KEY=sk_test_5DbmoNJeli74tMRgtzZLBY4SC6gpTLbNGnvAPeWJkE
SIGNING_SECRET=whsec_123
4. Make the Webhook Route Public
If using clerkMiddleware()
, ensure that /api/webhooks(.*)
is public. For information on configuring routes, see the clerkMiddleware() guide.
5. Install svix
Clerk uses svix
to send signed webhook requests. Install it:
npm install svix
6. Create the Webhook API Route
Create /src/app/api/webhooks/user/route.ts
to receive webhook requests:
import { NextResponse } from 'next/server';
import { Pool } from 'pg';
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { WebhookEvent } from '@clerk/nextjs/server';
// Initialize Neon DB connection
const pool = new Pool({
connectionString: process.env.DATABASE_URL, // Add to .env
});
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,
});
}
// Handling the user.created event (modify as needed)
try {
if (evt.type === 'user.created') {
const { id } = evt.data;
// Insert user into Neon DB
await pool.query(
`INSERT INTO users (user_id) VALUES ($1)
ON CONFLICT (user_id) DO NOTHING`,
[id]
);
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. Test the Webhook
- In the Clerk Webhook settings, go to Testing.
- Select user.created from Send event.
- Click Send Example.
- If the request succeeds, it will be marked as Succeeded in Message Attempts
Therefore your app can automatically register new user information in the database even when using Clerk's OAuth. Please try registering a new user within the app to confirm that it works correctly.
Debugging Tips
If you encounter issues:
- Check your Middleware configuration
- Check your configuration in the Clerk Dashboard [For example] Directory: /src/app*/api/webhooks/user/route.ts* Endpoint in Clerk : http://.../api/webhooks/user
- Test the Route Handler or API Route(following the guide) 1.Create a test route in your application:
export async function POST() {
return Response.json({ message: 'The route is working' });
}
2.Run your application.
3.Use curl
to test:
curl -H 'Content-Type: application/json' -X POST
http://localhost:3000/api/webhooks/test
4.If you see the {"message":"The route is working"}, then the basic Route Handler is working and ready to build on.
User deletion in Clerk
If you want to delete user information on Clerk, you can do so from the "User" tab.
Deploying
- Add the webhook URL in Clerk with the production domain.
- Add the Signing Secret to production environment variables.
- Deploy your app.
Conclusion
- Clerk webhooks enable real-time detection of new users.
- Ngrok allows local webhook testing by exposing an accessible endpoint.
- Implementing
svix
ensures request verification and security.
Top comments (0)