Skillshub clerk-core-workflow-a
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/clerk-core-workflow-a" ~/.claude/skills/comeonoliver-skillshub-clerk-core-workflow-a && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/clerk-core-workflow-a/SKILL.mdsource content
Clerk Core Workflow A: Sign-Up & Sign-In
Overview
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.
Prerequisites
- Clerk SDK installed and configured (
completed)clerk-install-auth - OAuth providers enabled in Dashboard > User & Authentication > Social Connections
- Sign-in/sign-up environment variables set
Instructions
Step 1: Pre-Built Components (Quick Start)
// 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> ) }
// 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> ) }
# .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
Step 2: Custom Sign-In Form (Full UI Control)
'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> ) }
Step 3: OAuth Social Login
'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> ) }
// app/sso-callback/page.tsx — handles OAuth redirect import { AuthenticateWithRedirectCallback } from '@clerk/nextjs' export default function SSOCallback() { return <AuthenticateWithRedirectCallback /> }
Step 4: Custom Sign-Up with Email Verification
'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> ) }
Step 5: Multi-Factor Authentication (TOTP)
'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 Handling
| Error | Cause | Solution |
|---|---|---|
| Email not registered | Show sign-up link or check Clerk instance |
| Wrong password | Display error, offer password reset link |
| Already signed in | Redirect to dashboard |
| Wrong or expired code | Allow retry; offer "Resend code" button |
| OAuth misconfigured | Check redirect URIs in Dashboard > Social Connections |
| Multi-session not enabled | Enable multi-session or redirect to dashboard |
Enterprise Considerations
- Customize component appearance with Clerk's
prop or custom CSS -- matches your design system without building from scratchappearance - For Enterprise SSO (SAML/OIDC), users authenticate through their IdP; the
component handles the redirect automatically<SignIn> - Phone number verification uses
strategy instead ofphone_codeemail_code - Magic link sign-in uses
strategy for passwordless authemail_link - Track sign-up conversion rates by subscribing to
webhooksuser.created - MFA enrollment is managed via
component or<UserProfile />
APIuser.createTOTP()
Resources
Next Steps
Proceed to
clerk-core-workflow-b for session management and middleware.