// docs / baas security / clerk hardening
Clerk security checklist: 20 items
Clerk handles auth, sessions, and organizations for your app — which means a misconfigured Clerk integration is an auth bypass, a session-fixation vector, or an org-leakage path. This checklist is a 20-item audit across keys, session config, webhooks, organizations, JWT templates, and ongoing monitoring. AI coding tools wire up Clerk quickly with sensible defaults; this list catches the items they leave on the table.
For background on why auth-layer misconfigurations are an AI-tooling weak spot, see <aiGaps>Why AI coding tools leave security gaps</aiGaps>. For the parallel checklist on Auth0, see <auth0Article>Auth0 security checklist</auth0Article>.
Environment keys and origin allowlist
Clerk issues two distinct keys per project. Mixing them or leaking them is the first failure mode.
- <strong>Use the publishable key (<code>pk_live_*</code> in production, <code>pk_test_*</code> in dev) in the browser; use the secret key (<code>sk_live_*</code> / <code>sk_test_*</code>) on the server only.</strong> The publishable key is safe in <code>NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY</code>; the secret key must never carry a public env prefix and must never appear in a client component.
- <strong>Verify the production app uses <code>pk_live_*</code>, not <code>pk_test_*</code>.</strong> Test instances allow unverified email addresses and disabled MFA — shipping test mode to production is an auth bypass.
- <strong>Configure the allowed origins in the Clerk Dashboard.</strong> Settings → Domains → Allowed origins must list your production domain exactly. Empty or wildcard origin lists let attackers create rogue Clerk frontends that talk to your backend.
- <strong>Rotate the secret key on any departure or suspected leak.</strong> Dashboard → API Keys → Reset. Old key is invalidated; redeploy server-side code with the new value before rotating.
Session configuration
Session expiry and idle timeouts are the difference between a stolen session being a 10-minute incident and a 30-day one.
- <strong>Set session inactivity timeout to 30 minutes or less for SaaS apps handling sensitive data.</strong> Dashboard → Sessions → Inactivity timeout. Banking-tier apps should use 5-10 minutes; standard SaaS 30-60 minutes; consumer apps 1-7 days. Default is 7 days.
- <strong>Enable session revocation on password change, email change, and MFA enrollment.</strong> Dashboard → Sessions → Revoke on. These are user-initiated security events; existing sessions on other devices should be killed.
- <strong>Verify sessions server-side on every protected route, not just at sign-in.</strong> In Next.js: <code>const '{' userId '}' = await auth();</code> in a server component / API route reads the JWT from the cookie and validates it. Never trust a cookie-only check.
- <strong>Set <code>SameSite=Lax</code> (default) or <code>Strict</code> on the session cookie.</strong> Verify in DevTools → Application → Cookies. <code>SameSite=None</code> is a CSRF vector — never use it unless you've explicitly configured a cross-domain auth setup.
Webhook verification
Clerk webhooks fire on user lifecycle events (created, updated, deleted, session.ended). They are the synchronization mechanism for your database — and a forged webhook is a database-write primitive.
- <strong>Verify the Svix signature on every webhook.</strong> Clerk webhooks are signed by Svix. Use <code>new Webhook(secret).verify(body, headers)</code>. Reject with <code>401</code> if verification fails.
- <strong>Store the webhook secret in an environment variable, never in code.</strong> The secret rotates on each Dashboard regeneration — your deploy must read it from env, not from a constant.
- <strong>Idempotency on every handler.</strong> Webhook deliveries can repeat. Use the <code>svix-id</code> header as a primary key in a <code>webhook_events</code> table to dedupe. Wrap the state change and the idempotency insert in the same transaction.
- <strong>On <code>user.deleted</code>, hard-delete or anonymize PII within 24 hours.</strong> GDPR / CCPA require it. Audit the deletion path: which tables hold this user's data? Use FK ON DELETE CASCADE where you can.
Organizations and permissions
If you use Clerk Organizations, the org boundary is your tenant isolation. Every server-side query must filter by it.
- <strong>On every API route, read both <code>userId</code> and <code>orgId</code> from <code>auth()</code> and filter database queries by both.</strong> <code>WHERE org_id = $orgId AND user_id = $userId</code>. Never trust an <code>org_id</code> from the request body.
- <strong>Use Clerk role checks for privileged operations, not boolean checks against the user object.</strong> <code>has({ role: 'org:admin' })</code> reads the role from the verified JWT. A user can spoof a boolean on a stale client object; they cannot spoof a JWT claim.
- <strong>Test cross-org isolation with two real org accounts.</strong> Create Org A, populate data, sign in to Org B in another browser, attempt to read Org A's data via the API. Response must be <code>403</code> or <code>404</code>.
JWT templates and external integrations
JWT templates push Clerk identity into Supabase, Firebase, and other downstream services. Misconfigured templates over-share claims or expose data you didn't mean to.
- <strong>For each JWT template, list every claim and confirm it is necessary.</strong> Dashboard → JWT Templates. A template that ships <code>email</code> and <code>phone</code> to Supabase exposes PII to anyone who reads the JWT in the browser.
- <strong>Set short expiry on JWT templates used for client-side downstream calls.</strong> 60 seconds for downstream API requests is the standard. Longer-lived JWTs are stolen and replayed.
- <strong>Verify the audience (<code>aud</code>) claim on the receiving side.</strong> Supabase, Firebase, etc. should check that <code>aud</code> matches the expected service identifier. Without this, a JWT issued for service A can authenticate to service B.
Operational monitoring
Auth is the highest-signal log source you have. Watch it.
- <strong>Alert on failed-login spikes per IP / per account.</strong> A 50× normal failure rate is a credential-stuffing attack. Clerk emits these events to webhooks; route them to your SIEM.
- <strong>Quarterly review of session and instance settings drift.</strong> Defaults change as Clerk updates; "old configurations" silently become wrong over time. Diff the Dashboard JSON export against your last-known-good copy.
Next steps
Run a FixVibe scan against your production URL — the <code>baas.clerk-auth0</code> check flags Clerk publishable keys, test keys in production, and bundled secret keys. For the equivalent checklist on Auth0, see <auth0Article>Auth0 security checklist</auth0Article>. For the umbrella view across BaaS providers, read <baasScannerArticle>BaaS misconfiguration scanner</baasScannerArticle>.
