You wired up Clerk for auth, and now you need your own database to know when a user signs up. The clean way to do that is a webhook: Clerk fires user.created, your backend catches it, and you upsert a row. Simple, until you try to build the handler on your laptop.
Clerk can only deliver webhooks to a public URL. Your handler lives on localhost:3000. So you deploy a half-finished endpoint to staging, click around in Clerk, tail remote logs, tweak one line, redeploy, and repeat. Every iteration costs minutes. And because Clerk signs payloads with Svix, you can't just fake a request with curl — you need the real headers to test your verification code.
Relayers closes that gap. It gives you a public ingest URL, forwards the original request untouched down a WebSocket tunnel to your machine, and lets you filter and reshape the payload before it ever hits your code. Signature verification keeps working because the bytes and headers arrive exactly as Clerk sent them.
What you'll build
Prerequisites
- A Clerk application with dashboard access.
- Your app running locally (this guide assumes
localhost:3000). - The Relayers desktop app or CLI installed. Grab it from relayers.app — the free tier covers this whole flow.
- Your Clerk webhook handler ready to receive
POSTrequests, ideally using the Svix library to verify signatures.
1Create an endpoint in Relayers
Open the Relayers desktop app and go to Endpoints → Create. Name it something like clerk-dev. Relayers generates a public ingest URL that looks like:
https://api.relayers.app/v1/webhooks/wep_abc123
Copy it. That wep_ identifier is your endpoint's public ID — it's the only address Clerk needs to know about. Use the Send test button now if you want to confirm the endpoint is live before touching Clerk.
2Add the endpoint in Clerk
In the Clerk Dashboard, go to Webhooks → Add Endpoint. Paste your Relayers URL into Endpoint URL, then subscribe to the events you care about. For user sync, start with:
user.createduser.updated
You might also want session.created or organization.created depending on your app. Save the endpoint.
Under the hood, Clerk uses Svix to deliver webhooks. That means every request carries three signing headers:
svix-id: msg_2abc...
svix-timestamp: 1720000000
svix-signature: v1,g0h1...
Clerk also shows you a Signing Secret (it starts with whsec_). Copy that into your local environment — your handler uses it, together with the svix-* headers, to verify that the request genuinely came from Clerk. Relayers forwards all three headers unchanged, so this verification works identically on localhost and in production.
3Forward events to localhost
Log in and start the tunnel from your terminal:
relayers login
relayers listen --forward localhost:3000
That's it. Relayers now pipes anything hitting wep_abc123 straight to your local dev server, preserving the method, path body, and every header. Trigger a real event by signing up a test user in your app, or hit Replay in the Clerk dashboard on a past delivery.
Watch the request land in the Relayers Events view. Open the payload inspector to see the full body and headers — including the svix-* set — exactly as your handler receives them.
4Route and filter with JQ
Clerk sends every subscribed event to the same URL, with the event name in the type field. If your user.created handler only cares about new users, filter noise out at the edge instead of in your code.
In the Relayers Rules tab, add a rule with a JQ filter:
{ "type": "user.created" }
Set the destination to your tunnel (localhost:3000). Rules match top to bottom and first match wins, so you can layer more specific routes above general ones and reorder them by dragging. For example, route user.created and user.updated to your sync handler, and send session.created somewhere else entirely.
5Transform the payload
Clerk's payloads are deep — data.email_addresses is an array, IDs are nested, and there's a lot you don't need. Instead of writing mapping code in every handler, reshape the event once in the Transformations tab.
Here's a JQ transform that flattens a user.created event into exactly what your DB upsert needs:
{
event: .type,
userId: .data.id,
email: .data.email_addresses[0].email_address
}
Relayers supports jq, jsonata, javascript, and go_template, and the built-in test playground lets you paste a sample Clerk payload and see the output before you save. Your local handler now receives a tidy, predictable shape.
One caveat: if you transform the body, the payload no longer matches the original
svix-signature. During development that's usually fine — you're testing your DB logic, not the signature. When you need to verify signatures, forward the untouched body (skip the transform, or keep signature verification and transformation on separate endpoints).
Signature verification, unchanged
This is the part that trips people up with other tunneling tools. Relayers forwards the original request body and headers untouched, so the svix-id, svix-timestamp, and svix-signature headers arrive byte-for-byte. Your Svix verification code — the same code you'll run in production — passes locally against real, signed events. No mocking, no bypass flags, no "it works on my machine but breaks in prod" surprises.
Going to production
When you ship, you don't need Relayers in the request path at all — point Clerk directly at your deployed URL. But keep the Relayers endpoint around: it's the fastest way to reproduce a production bug locally. Use Import/Export to save your endpoint, rules, and transformations as a JSON bundle so a teammate can spin up the identical setup in seconds.
If you prefer to keep Relayers in production as a routing layer, swap the tunnel destination for a public URL in the rule and you're done — same filters, same transforms.
Troubleshooting
- Svix verification fails locally. Confirm you copied the signing secret from the same Clerk endpoint you registered with Relayers, and that no transform is altering the body. Check the payload inspector to verify the
svix-*headers actually arrived. - No events showing up. Make sure
relayers listenis running and the tunnel is connected. Then confirm you actually subscribed to the event in Clerk — a missinguser.createdsubscription is the most common cause. - Need to re-fire an event. Every Clerk webhook delivery has a Replay button in the dashboard. Use it to resend without creating new users. Relayers also keeps its own event history with a retry action in the Events view.
- Wrong handler getting the event. Remember first match wins in Rules — reorder so your specific filters sit above catch-all rules.
Wrap-up
Syncing Clerk users shouldn't mean redeploying to test one line. With a Relayers endpoint in front of your Clerk webhook, you get a public URL, a tunnel to localhost, JQ routing, payload transforms, and — critically — Svix signatures that verify locally exactly as they will in production.
Create your first endpoint at relayers.app and download the app to start catching Clerk events on localhost in minutes.