Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement user sign-up and sign-in flows with Clerk. Use when building authentication UI, customizing sign-in experience, or implementing OAuth social login. Trigger with phrases like "clerk sign-in", "clerk sign-up", "clerk login flow", "clerk OAuth", "clerk social login".
.claude/skills/jeremylongshore-clerk-core-workflow-a/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 404% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 204% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 243% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 213% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 273% | 0% |
Implement authentication flows with Clerk: pre-built components for rapid setup, custom forms for full UI control, OAuth social login, email/phone verification, and multi-factor authentication.
clerk-install-auth completed)typescript// app/sign-in/[[...sign-in]]/page.tsx import { SignIn } from '@clerk/nextjs' export default function SignInPage() { return ( <div className="flex min-h-screen items-center justify-center"> <SignIn appearance={{ elements: { rootBox: 'mx-auto', card: 'shadow-xl rounded-2xl', headerTitle: 'text-2xl', socialButtonsBlockButton: 'rounded-lg', }, }} routing="path" path="/sign-in" signUpUrl="/sign-up" /> </div> ) }
typescript// app/sign-up/[[...sign-up]]/page.tsx import { SignUp } from '@clerk/nextjs' export default function SignUpPage() { return ( <div className="flex min-h-screen items-center justify-center"> <SignUp routing="path" path="/sign-up" signInUrl="/sign-in" /> </div> ) }
bash# .env.local — routing configuration NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
typescript'use client' import { useSignIn } from '@clerk/nextjs' import { useRouter } from 'next/navigation' import { useState } from 'react' export function CustomSignIn() { const { signIn, setActive, isLoaded } = useSignIn() const router = useRouter() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) if (!isLoaded) return null const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') setLoading(true) try { const result = await signIn.create({ identifier: email, password, }) if (result.status === 'complete') { // Session created — activate it and redirect await setActive({ session: result.createdSessionId }) router.push('/dashboard') } else if (result.status === 'needs_second_factor') { // MFA required — handled in Step 5 router.push('/sign-in/mfa') } else if (result.status === 'needs_identifier') { setError('Please enter your email address') } } catch (err: any) { const clerkError = err.errors?.[0] switch (clerkError?.code) { case 'form_identifier_not_found': setError('No account found with this email') break case 'form_password_incorrect': setError('Incorrect password') break case 'session_exists': router.push('/dashboard') break default: setError(clerkError?.message || 'Sign-in failed') } } finally { setLoading(false) } } return ( <form onSubmit={handleSubmit} className="space-y-4 max-w-sm mx-auto"> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email address" required className="w-full p-2 border rounded" /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" required className="w-full p-2 border rounded" /> {error && <p className="text-red-500 text-sm">{error}</p>} <button type="submit" disabled={loading} className="w-full p-2 bg-blue-600 text-white rounded"> {loading ? 'Signing in...' : 'Sign In'} </button> </form> ) }
typescript'use client' import { useSignIn } from '@clerk/nextjs' export function OAuthButtons() { const { signIn, isLoaded } = useSignIn() if (!isLoaded) return null const signInWith = (strategy: 'oauth_google' | 'oauth_github' | 'oauth_apple') => { signIn.authenticateWithRedirect({ strategy, redirectUrl: '/sso-callback', redirectUrlComplete: '/dashboard', }) } return ( <div className="flex flex-col gap-3"> <button onClick={() => signInWith('oauth_google')} className="p-2 border rounded"> Continue with Google </button> <button onClick={() => signInWith('oauth_github')} className="p-2 border rounded"> Continue with GitHub </button> <button onClick={() => signInWith('oauth_apple')} className="p-2 border rounded"> Continue with Apple </button> </div> ) }
typescript// app/sso-callback/page.tsx — handles OAuth redirect import { AuthenticateWithRedirectCallback } from '@clerk/nextjs' export default function SSOCallback() { return <AuthenticateWithRedirectCallback /> }
typescript'use client' import { useSignUp } from '@clerk/nextjs' import { useRouter } from 'next/navigation' import { useState } from 'react' export function CustomSignUp() { const { signUp, setActive, isLoaded } = useSignUp() const router = useRouter() const [step, setStep] = useState<'form' | 'verify'>('form') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [code, setCode] = useState('') const [error, setError] = useState('') if (!isLoaded) return null const handleSignUp = async (e: React.FormEvent) => { e.preventDefault() try { await signUp.create({ emailAddress: email, password }) // Send verification email await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }) setStep('verify') } catch (err: any) { setError(err.errors?.[0]?.longMessage || 'Sign-up failed') } } const handleVerify = async (e: React.FormEvent) => { e.preventDefault() try { const result = await signUp.attemptEmailAddressVerification({ code }) if (result.status === 'complete') { await setActive({ session: result.createdSessionId }) router.push('/onboarding') } } catch (err: any) { setError(err.errors?.[0]?.longMessage || 'Verification failed') } } if (step === 'verify') { return ( <form onSubmit={handleVerify} className="space-y-4"> <p>Check your email for a verification code</p> <input value={code} onChange={e => setCode(e.target.value)} placeholder="Enter 6-digit code" required /> {error && <p className="text-red-500">{error}</p>} <button type="submit">Verify Email</button> </form> ) } return ( <form onSubmit={handleSignUp} className="space-y-4"> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" required /> <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" required /> {error && <p className="text-red-500">{error}</p>} <button type="submit">Create Account</button> </form> ) }
typescript'use client' import { useSignIn } from '@clerk/nextjs' import { useRouter } from 'next/navigation' import { useState } from 'react' export function MFAVerification() { const { signIn, setActive } = useSignIn() const router = useRouter() const [code, setCode] = useState('') const [error, setError] = useState('') const handleMFA = async (e: React.FormEvent) => { e.preventDefault() try { const result = await signIn!.attemptSecondFactor({ strategy: 'totp', code, }) if (result.status === 'complete') { await setActive!({ session: result.createdSessionId }) router.push('/dashboard') } } catch (err: any) { setError(err.errors?.[0]?.message || 'Invalid code') } } return ( <form onSubmit={handleMFA} className="space-y-4"> <h2>Two-Factor Authentication</h2> <p>Enter the code from your authenticator app</p> <input value={code} onChange={e => setCode(e.target.value)} placeholder="6-digit TOTP code" maxLength={6} required inputMode="numeric" pattern="[0-9]*" /> {error && <p className="text-red-500">{error}</p>} <button type="submit">Verify</button> </form> ) }
| Error | Cause | Solution | |-------|-------|----------| | form_identifier_not_found | Email not registered | Show sign-up link or check Clerk instance | | form_password_incorrect | Wrong password | Display error, offer password reset link | | session_exists | Already signed in | Redirect to dashboard | | verification_failed | Wrong or expired code | Allow retry; offer "Resend code" button | | oauth_callback_error | OAuth misconfigured | Check redirect URIs in Dashboard > Social Connections | | identifier_already_signed_in | Multi-session not enabled | Enable multi-session or redirect to dashboard |
appearance prop or custom CSS -- matches your design system without building from scratch<SignIn> component handles the redirect automaticallyphone_code strategy instead of email_codeemail_link strategy for passwordless authuser.created webhooks<UserProfile /> component or user.createTOTP() APIProceed to clerk-core-workflow-b for session management and middleware.
CLERK_*) and session verification pathUser: Wire Clerk auth into my Next.js App Router app with protected /dashboard.
Skill: installs Clerk packages, adds middleware, wraps layout with ClerkProvider,
protects /dashboard, documents CLERK_SECRET_KEY + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY.User: Users bounce between /sign-in and /dashboard in a loop.
Skill: traces middleware matcher, publicRoutes, and afterSignInUrl; fixes redirect cycle.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 6,164 | 4,788 | -22% | 1 | 1 | 0% | 1,167 | 4,005 | +243% | 0 | 0 | — |
case-02 | pass→pass | 6,549 | 5,280 | -19% | 1 | 1 | 0% | 1,309 | 4,101 | +213% | 0 | 0 | — |
case-03 | pass→pass | 5,629 | 5,495 | -2% | 1 | 1 | 0% | 1,114 | 4,151 | +273% | 0 | 0 | — |
case-04 | pass→pass | 4,049 | 2,557 | -37% | 1 | 1 | 0% | 722 | 3,468 | +380% | 0 | 0 | — |
case-05 | fail→pass | 3,717 | 1,973 | -47% | 1 | 1 | 0% | 675 | 3,405 | +404% | 0 | 0 | — |
case-06 | pass→pass | 5,257 | 2,697 | -49% | 1 | 1 | 0% | 995 | 3,541 | +256% | 0 | 0 | — |
case-07 | fail→pass | 6,949 | 4,303 | -38% | 1 | 1 | 0% | 1,280 | 3,887 | +204% | 0 | 0 | — |
case-08 | pass→pass | 4,869 | 2,757 | -43% | 1 | 1 | 0% | 956 | 3,466 | +263% | 0 | 0 | — |
case-09 | pass→pass | 10,153 | 9,749 | -4% | 1 | 1 | 0% | 2,232 | 5,112 | +129% | 0 | 0 | — |
case-10 | pass→pass | 2,908 | 1,845 | -37% | 1 | 1 | 0% | 491 | 3,383 | +589% | 0 | 0 | — |
case-11 | pass→pass | 3,638 | 2,131 | -41% | 1 | 1 | 0% | 517 | 3,354 | +549% | 0 | 0 | — |
case-12 | pass→pass | 5,193 | 1,816 | -65% | 1 | 1 | 0% | 722 | 3,363 | +366% | 0 | 0 | — |
case-13 | pass→pass | 4,223 | 2,908 | -31% | 1 | 1 | 0% | 786 | 3,477 | +342% | 0 | 0 | — |
case-14 | pass→pass | 7,951 | 3,838 | -52% | 1 | 1 | 0% | 1,462 | 3,710 | +154% | 0 | 0 | — |
case-15 | pass→pass | 4,334 | 1,810 | -58% | 1 | 1 | 0% | 785 | 3,364 | +329% | 0 | 0 | — |
case-16 | pass→pass | 5,006 | 2,984 | -40% | 1 | 1 | 0% | 930 | 3,616 | +289% | 0 | 0 | — |
case-17 | pass→pass | 10,367 | 4,843 | -53% | 1 | 1 | 0% | 1,854 | 3,954 | +113% | 0 | 0 | — |
case-18 | pass→pass | 5,927 | 2,547 | -57% | 1 | 1 | 0% | 936 | 3,529 | +277% | 0 | 0 | — |
case-19 | pass→pass | 17,946 | 6,589 | -63% | 1 | 1 | 0% | 2,078 | 4,205 | +102% | 0 | 0 | — |
case-20 | pass→pass | 8,116 | 7,969 | -2% | 1 | 1 | 0% | 1,621 | 4,605 | +184% | 0 | 0 | — |
case-21 | pass→pass | 9,883 | 11,362 | +15% | 1 | 1 | 0% | 2,112 | 5,413 | +156% | 0 | 0 | — |
case-22 | pass→pass | 13,922 | 32,825 | +136% | 1 | 1 | 0% | 2,898 | 6,614 | +128% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.