Where the trust boundary is
Clerk's components and hooks run in the browser and make the app look signed in or signed out. That is presentation. Clerk's documentation says its Show component only visually hides its children when the user is not authorised, and that the contents remain accessible via the browser's source code. The decision that counts happens on your server, where Clerk's helpers give you the verified user id and you decide whether this user may touch this record.
Clerk's current middleware reference says middleware is not the best place to protect routes, and that you should protect access as close to the resource as possible, in the code that reads or mutates the data. It also marks the createRouteMatcher helper as deprecated. Many AI-generated projects still use a list of protected route patterns in middleware as the only guard, so check which approach your project has and whether anything sits outside the list.
Clerk knows nothing about your tables. A signed-in user who sends another user's record id to your API is fully authenticated. Whether they get the record depends on a line of your own code.
The checklist
| Area | What to verify | How to test on your own project | Pass condition |
|---|---|---|---|
| Server-side checks | Every page, route handler and server action that touches private data checks the user on the server. | Call each API route with curl and no session cookie, using the example in Step 1. | Private routes refuse anonymous calls. |
| Ownership | Queries are scoped to the verified user id or organisation. | Sign in as a second test user and request the first user's record id. | The second user gets a refusal or an empty result. |
| Keys | Only the publishable key reaches the browser. | Search env files and the built output for sk_test_ and sk_live_. | The secret key has no public prefix and does not appear in any browser file. |
| Metadata and roles | Roles and entitlements are read from public or private metadata, never unsafe metadata. | Search the code for unsafeMetadata and read every place it is used. | No access decision depends on a value the user can edit. |
| Webhooks | The webhook route verifies the signature before acting. | Send a fake event to your own endpoint with curl, as in Step 3. | The unsigned request is rejected and nothing is written. |
| Sign-up restrictions | The sign-up mode matches who should be able to create an account. | Review the restrictions settings in the Clerk dashboard. | Internal tools are not open to public sign-up. |
| Production instance | The live site uses production keys, your own OAuth credentials and authorised parties. | Check the key prefixes in your host's environment settings and read your middleware options. | Keys start with pk_live_ and sk_live_, and authorizedParties is set. |
Step 1: Check the user next to the data
Clerk's Next.js documentation shows auth.protect() and the auth() helper for pages, route handlers and server actions. The example below is the documented shape for a route handler, extended with the step Clerk cannot do for you: using the verified user id to scope the query. Compare your own handlers with it, then call each route against your own site with no cookie.
import { auth } from '@clerk/nextjs/server'
import { db } from '@/lib/db'
export const GET = async () => {
const { userId } = await auth.protect()
const notes = await db.note.findMany({ where: { ownerId: userId } })
return Response.json({ notes })
}
export SITE="https://your-site.example"
curl -i "$SITE/api/notes"Step 2: Keys and metadata
Clerk's documentation describes two keys. The publishable key is safe for the front end and starts with pk_test_ or pk_live_. The secret key starts with sk_test_ or sk_live_, and the documentation says not to expose it on the front end with a public environment variable. In a Next.js project that means the secret key must never have a NEXT_PUBLIC_ name.
User metadata comes in three kinds. Clerk's documentation says public metadata can be read from the front end but written only from the backend, private metadata is backend-only, and unsafe metadata can be read and written from the front end. It warns that malicious users could tamper with unsafe metadata. Clerk's role guide stores roles in public metadata for that reason. If generated code reads a role or a paid flag from unsafe metadata, any user can promote themselves.
grep -rn "CLERK_SECRET_KEY" .env* 2>/dev/null
grep -rnE "NEXT_PUBLIC_.*(SECRET|sk_)" .env* 2>/dev/null
grep -rn "unsafeMetadata" src app
npm run build
grep -rEl "sk_live_|sk_test_" .next/staticStep 3: Verify webhooks
Apps often sync Clerk users into their own database through webhooks. Clerk's documentation says the webhook route must be public, because incoming events carry no session, and shows verifying each request with verifyWebhook() and a signing secret stored as CLERK_WEBHOOK_SIGNING_SECRET. A public route that skips verification lets anyone create or alter users in your database.
Test your own endpoint with a made-up event. It should be rejected. The documentation also notes that webhook delivery can be delayed or fail and is retried, so handlers should cope with repeats.
curl -i -X POST "$SITE/api/webhooks/clerk" \
-H "Content-Type: application/json" \
-d '{"type":"user.created","data":{"id":"user_fake"}}'Step 4: Sign-up and production settings
Clerk's documentation lists three sign-up modes: public, which is the default and open to anyone, restricted, where admins control access, and waitlist. It also describes an allowlist and a blocklist for identifiers. If your app is for a fixed group, open sign-up combined with a check that only asks whether someone is signed in gives every stranger access.
Before launch, read Clerk's production deployment guide. It says development and production instances have different keys, that the shared OAuth credentials Clerk provides in development are not secure for production and you need your own, and it recommends setting the authorizedParties option to protect against a compromised app on another subdomain. Confirm each of these in your own dashboard and code.
Common mistakes
- Treating signed in as allowed. Authentication is not authorisation over your own records.
- Relying only on a route pattern list in middleware, then adding an API route outside it.
- Taking the user id from the request body instead of from Clerk's server helper.
- Storing a role or a paid flag in unsafe metadata because it was easy to write from the client.
- Leaving the webhook route unverified because it had to be public.
- Going live on development keys and shared OAuth credentials.
- Leaving public sign-up on for an internal tool.
Keep it working
Repeat the anonymous probe and the two-account test whenever a route or action is added. Re-read the restrictions and key settings before each launch. Pass condition: anonymous calls are refused, the second user cannot reach the first user's records, no access decision uses unsafe metadata, and unsigned webhook events are rejected.
Frequently asked questions
Is Clerk secure?
Clerk handles sign-in and sessions for you, which removes a lot of risky home-made code. Whether your app is secure depends on how you use it: server-side checks next to your data, correct key placement, verified webhooks and production settings. Those are the parts this checklist tests.
Does Clerk handle authorisation?
Only in part. Clerk offers roles, permissions and helpers such as has() and auth.protect() to check them. It has no knowledge of your database, so deciding whether a user may read a specific record is still your code. Scope every query to the verified user id or organisation.
Is Clerk middleware enough to protect my routes?
No, do not rely on it alone. Clerk's middleware reference says middleware is not the best place to protect routes and recommends protecting access as close to the resource as possible, in the code that reads or mutates the data.
Is the Clerk publishable key safe to expose?
Yes. Clerk's documentation describes the publishable key as the one for front-end use. The secret key is different, and the documentation says not to expose it on the front end with a public environment variable.
Can users edit their own Clerk metadata?
They can edit unsafe metadata. Clerk's documentation says unsafe metadata is readable and writable from the front end and warns that it can be tampered with. Public metadata is readable from the front end but writable only from the backend, so roles belong there or in private metadata.
Sources
- 1.Clerk docs: clerkMiddleware() for Next.js
- 2.Clerk docs: Protect content from unauthenticated users (Next.js)
- 3.Clerk docs: Authorization checks
- 4.Clerk docs: User metadata
- 5.Clerk docs: Sync Clerk data with webhooks
- 6.Clerk docs: Restricting access
- 7.Clerk docs: Deploy to production
- 8.OWASP Cheat Sheet: Authorization