Usage & Integration

Practical examples of using Clerk authentication in Plainform components, pages, and API routes

Learn how to use Clerk authentication in your Plainform application with practical examples.

Server Components

Access user data in Server Components using auth() or currentUser():

app/dashboard/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  // Get user ID only (faster)
  const { userId } = await auth();

  if (!userId) {
    redirect('/sign-in');
  }

  // Get full user object when needed
  const user = await currentUser();

  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Email: {user.emailAddresses[0].emailAddress}</p>
    </div>
  );
}

Check Permissions

Verify user roles or permissions:

app/admin/page.tsx
import { auth } from '@clerk/nextjs/server';

export default async function AdminPage() {
  const { userId, has } = await auth();

  if (!userId || !has({ role: 'admin' })) {
    return <div>Access denied</div>;
  }

  return <div>Admin dashboard</div>;
}

Client Components

Use Clerk hooks in Client Components for reactive auth state:

components/UserProfile.tsx
'use client';

import { useUser } from '@clerk/nextjs';

export function UserProfile() {
  const { user, isLoaded, isSignedIn } = useUser();

  if (!isLoaded) return <div>Loading...</div>;
  if (!isSignedIn) return <div>Please sign in</div>;

  return (
    <div className="flex items-center gap-3">
      <img
        src={user.imageUrl}
        alt={user.fullName || 'User'}
        className="h-10 w-10 rounded-full"
      />
      <div>
        <p className="font-semibold">{user.fullName}</p>
        <p className="text-sm text-muted-foreground">
          {user.primaryEmailAddress?.emailAddress}
        </p>
      </div>
    </div>
  );
}

Conditional Rendering

Show different content based on auth state:

components/AuthContent.tsx
'use client';

import { useAuth } from '@clerk/nextjs';
import Link from 'next/link';

export function AuthContent() {
  const { isSignedIn, isLoaded } = useAuth();

  if (!isLoaded) return <div>Loading...</div>;

  return isSignedIn ? (
    <Link href="/dashboard">Go to Dashboard</Link>
  ) : (
    <div className="flex gap-4">
      <Link href="/sign-up">Create Account</Link>
      <Link href="/sign-in">Sign In</Link>
    </div>
  );
}

API Routes

Protect API routes and verify authentication:

app/api/user/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId } = await auth();

  if (!userId) {
    return new NextResponse('Unauthorized', { status: 401 });
  }

  const userData = await getUserData(userId);
  return NextResponse.json(userData);
}

Clerk Webhook

Plainform also includes a webhook route at app/api/webhooks/clerk/route.ts. It verifies Clerk webhook signatures and syncs user.created, user.updated, and user.deleted events into the local User table.

Use the dedicated webhook page when setting up local tunneling, Clerk Dashboard events, and the CLERK_WEBHOOK_SECRET:

Role-Based Access

Restrict API access by role:

app/api/admin/users/route.ts
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
  const { userId, has } = await auth();

  if (!userId || !has({ role: 'admin' })) {
    return new NextResponse('Forbidden', { status: 403 });
  }

  const users = await getAllUsers();
  return NextResponse.json(users);
}

Server Actions

Verify authentication in Server Actions:

actions/updateProfile.ts
'use server';

import { auth } from '@clerk/nextjs/server';
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma/prisma';

export async function updateProfile(formData: FormData) {
  const { userId } = await auth();

  if (!userId) {
    throw new Error('Unauthorized');
  }

  const name = formData.get('name') as string;

  await prisma.user.update({
    where: { clerkId: userId },
    data: { name },
  });

  revalidatePath('/profile');
  return { success: true };
}

Custom Authentication Forms

Plainform uses custom sign-in and sign-up forms built with Clerk hooks for full design control. These are located in components/user/.

Plainform uses Clerk authentication hooks (useSignIn, useSignUp) with custom UI components. No pre-built Clerk UI components are used for authentication flows.

Sign-In Flow

The sign-in form uses the useSignIn hook:

components/user/SignInForm.tsx (simplified)
'use client';

import { useSignIn } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';

export function SignInForm() {
  const { isLoaded, signIn, setActive } = useSignIn();
  const router = useRouter();
  const { register, handleSubmit, setError } = useForm();

  const onSubmit = async (data) => {
    const { identifier, password } = data;

    if (!isLoaded) return;

    try {
      const signInAttempt = await signIn.create({
        identifier,
        password,
      });

      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });
        router.push('/');
        window.location.reload();
      }
    } catch (err) {
      err.errors.forEach((error) => {
        const field = error.meta?.paramName;
        if (field && error.longMessage) {
          setError(field, { type: 'manual', message: error.longMessage });
        }
      });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('identifier')} type="email" placeholder="Email" />
      <input {...register('password')} type="password" placeholder="Password" />
      <button type="submit" disabled={isLoading}>
        Sign In
      </button>
    </form>
  );
}

Key features:

  • signIn.create() - Authenticates with identifier and password
  • setActive() - Activates the returned Clerk session
  • isLoaded - Prevents calls before Clerk has initialized
  • Clerk API errors - Mapped into React Hook Form field errors

Sign-Up Flow with Email Verification

The sign-up form uses useSignUp hook with email verification:

components/user/SignUpForm.tsx (simplified)
'use client';

import { useSignUp } from '@clerk/nextjs';
import { useForm } from 'react-hook-form';

export function SignUpForm() {
  const [verifying, setVerifying] = useState(false);
  const { isLoaded, signUp } = useSignUp();
  const { register, handleSubmit, setError } = useForm();

  const onSubmit = async (data) => {
    const { email_address, password } = data;

    if (!isLoaded) return;

    try {
      await signUp.create({
        emailAddress: email_address,
        password,
      });

      await signUp.prepareEmailAddressVerification({
        strategy: 'email_code',
      });

      setVerifying(true);
    } catch (err) {
      err.errors.forEach((error) => {
        const field = error.meta?.paramName;
        if (field && error.longMessage) {
          setError(field, { type: 'manual', message: error.longMessage });
        }
      });
    }
  };

  if (verifying) return <SignUpVerificationForm />;

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email_address')} type="email" placeholder="Email" />
      <input {...register('password')} type="password" placeholder="Password" />
      <button type="submit" disabled={isLoading}>
        Sign Up
      </button>
    </form>
  );
}

Key features:

  • signUp.create() - Creates the account with email and password
  • signUp.prepareEmailAddressVerification() - Sends the email verification code
  • Local verifying state - Switches from the sign-up form to the verification form
  • Clerk API errors - Mapped into React Hook Form field errors

Email Verification Form

The verification form handles email code verification:

components/user/SignUpVerificationForm.tsx (simplified)
'use client';

import { useSignUp } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

export function SignUpVerificationForm() {
  const { isLoaded, signUp, setActive } = useSignUp();
  const router = useRouter();

  const onSubmit = async (data) => {
    const { code } = data;

    if (!isLoaded) return;

    const signUpAttempt = await signUp.attemptEmailAddressVerification({
      code,
    });

    if (signUpAttempt.status === 'complete') {
      await setActive({ session: signUpAttempt.createdSessionId });

      if (signUpAttempt.emailAddress) {
        await addContact(signUpAttempt.emailAddress);
      }

      router.push('/');
    }
  };

  const resendCode = async () => {
    await signUp.prepareEmailAddressVerification({
      strategy: 'email_code',
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="Enter 6-digit code" maxLength={6} />
      <button type="submit" disabled={isLoading}>
        Verify
      </button>
      <button type="button" onClick={resendCode}>
        Resend Code
      </button>
    </form>
  );
}

Key features:

  • signUp.attemptEmailAddressVerification() - Verifies the email code
  • setActive() - Activates the created session
  • addContact() - Adds the verified email to the newsletter/contact list
  • signUp.prepareEmailAddressVerification() - Resends the verification code

Password Reset Flow

The password reset flow uses two forms:

1. Request Reset Code:

components/user/ForgotPasswordForm.tsx (simplified)
'use client';

import { useSignIn } from '@clerk/nextjs';

export function ForgotPasswordForm() {
  const { isLoaded, signIn } = useSignIn();
  const { handleSubmit, formState: { isSubmitting } } = useForm();

  const sendResetCode = async (data) => {
    const { identifier } = data;

    if (!isLoaded) return;

    await signIn.create({
      strategy: 'reset_password_email_code',
      identifier,
    });

    setCodeSent(true); // Show reset form
  };

  return (
    <form onSubmit={handleSubmit(sendResetCode)}>
      <input type="email" placeholder="Email" />
      <button type="submit" disabled={isSubmitting}>
        Send Reset Code
      </button>
    </form>
  );
}

2. Reset Password with Code:

components/user/ResetPasswordForm.tsx (simplified)
'use client';

import { useSignIn } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

export function ResetPasswordForm() {
  const { isLoaded, signIn, setActive } = useSignIn();
  const router = useRouter();
  const { handleSubmit, formState: { isSubmitting } } = useForm();

  const onSubmit = async (data) => {
    const { code, password } = data;

    if (!isLoaded) return;

    const resetPasswordAttempt = await signIn.attemptFirstFactor({
      strategy: 'reset_password_email_code',
      password,
      code,
    });

    if (resetPasswordAttempt.status === 'complete') {
      await setActive({ session: resetPasswordAttempt.createdSessionId });
      router.push('/');
    }
  };

  const resendCode = async () => {
    await signIn.create({
      strategy: 'reset_password_email_code',
      identifier: signIn.identifier,
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="text" placeholder="Enter 6-digit code" maxLength={6} />
      <input type="password" placeholder="New password" />
      <input type="password" placeholder="Confirm password" />
      <button type="submit" disabled={isSubmitting}>
        Reset Password
      </button>
      <button type="button" onClick={resendCode}>
        Resend Code
      </button>
    </form>
  );
}

Key features:

  • signIn.create({ strategy: 'reset_password_email_code' }) - Sends the reset code
  • signIn.attemptFirstFactor() - Verifies the code and submits the new password
  • setActive() - Activates the session after a successful password reset

OAuth Authentication

The OAuth connection component uses Clerk's redirect-based OAuth API. Here's the complete implementation:

components/user/OAuthConnection.tsx (simplified)
'use client';

import { useSignIn, useSignUp } from '@clerk/nextjs';
import { OAuthStrategy } from '@clerk/types';
import { useState } from 'react';

interface IOAuthConnection {
  isSignedUp?: boolean;                       // Optional: true on sign-up form
  strategy: 'oauth_google' | 'oauth_github';  // Required: OAuth provider strategy
  children: React.ReactNode;                   // Required: Button text content
  icon: string;                                // Required: Icon name for the button
  lastUsed?: string | null;                    // Optional: Last used strategy for badge
  className?: string;                          // Optional: Additional CSS classes
}

export function OAuthConnection({
  strategy,
  children,
  lastUsed,
  icon,
  isSignedUp,
}: IOAuthConnection) {
  const { signIn } = useSignIn();
  const { signUp } = useSignUp();
  const [loadingProvider, setLoadingProvider] = useState<string | null>(null);

  async function signInWith(strategy: OAuthStrategy) {
    setLoadingProvider(strategy);
    return signIn?.authenticateWithRedirect({
      strategy,
      redirectUrl: '/sso-callback',
      redirectUrlComplete: '/',
    });
  }

  async function signUpWith(strategy: OAuthStrategy) {
    setLoadingProvider(strategy);
    return signUp?.authenticateWithRedirect({
      strategy,
      redirectUrl: '/sso-callback',
      redirectUrlComplete: '/',
    });
  }

  return (
    <button
      onClick={() => (!isSignedUp ? signInWith(strategy) : signUpWith(strategy))}
      disabled={loadingProvider === strategy}
    >
      <SvgFinder icon={icon} />
      {children}
      {lastUsed === strategy && <span>Last used</span>}
    </button>
  );
}

Usage example:

Using OAuthConnection in your forms
import { OAuthConnection } from '@/components/user/OAuthConnection';
import { useClerk } from '@clerk/nextjs';

export function SignInForm() {
  const { client } = useClerk();
  const lastStrategy = client?.lastAuthenticationStrategy;

  return (
    <div className="flex flex-col gap-4">
      <OAuthConnection
        strategy="oauth_google"    // Required: Must match Clerk's strategy name
        icon="Google"              // Required: Icon name from SvgFinder
        lastUsed={lastStrategy}    // Optional: Shows "Last used" badge
      >
        Continue with Google       {/* Required: Button text */}
      </OAuthConnection>

      <OAuthConnection
        strategy="oauth_github"
        icon="GitHub"
        lastUsed={lastStrategy}
      >
        Continue with GitHub
      </OAuthConnection>
    </div>
  );
}

Required Props:

  • strategy: OAuth provider identifier (e.g., "oauth_google", "oauth_github", "oauth_microsoft")
    • Must exactly match Clerk's strategy names
    • See Clerk's documentation for all available providers
  • icon: Icon name to display on the button
    • Must exist in your SvgFinder component
  • children: Button text content (e.g., "Continue with Google")

Optional Props:

  • lastUsed: Last authentication strategy used
    • Shows a "Last used" badge when it matches the current strategy
    • Get from useClerk().client?.lastAuthenticationStrategy
  • isSignedUp: Use this on sign-up forms so the component starts the sign-up OAuth flow
  • className: Additional CSS classes for custom styling

Key features:

  • signIn.authenticateWithRedirect() - Initiates OAuth from the sign-in form
  • signUp.authenticateWithRedirect() - Initiates OAuth from the sign-up form
  • loadingProvider - Local state to track which provider is currently loading
  • redirectUrl - Where the OAuth provider redirects after authentication (/sso-callback)
  • redirectUrlComplete - Final destination after successful authentication (/)
  • Explicit flow selection - isSignedUp switches the button from sign-in OAuth to sign-up OAuth

The actual implementations in components/user/ include additional features like form validation with Zod, styled UI components with Radix, BeatLoader spinner, and comprehensive error handling with toast notifications. These examples show the core Clerk integration patterns.

Pre-Built Components

User Profile

Use Clerk's <UserProfile> component for profile management:

app/profile/page.tsx
import { UserProfile } from '@clerk/nextjs';

export default function ProfilePage() {
  return (
    <div className="flex justify-center p-8">
      <UserProfile
        appearance={{
          elements: {
            rootBox: 'w-full max-w-2xl',
            card: 'shadow-lg',
          },
        }}
      />
    </div>
  );
}

User Button

Add a user menu button to your navigation:

components/Navigation.tsx
'use client';

import { UserButton } from '@clerk/nextjs';

export function Navigation() {
  return (
    <nav className="flex items-center justify-between p-4">
      <div>Logo</div>
      <UserButton />
    </nav>
  );
}

Session Tokens

Access session tokens for external API calls:

components/ApiCall.tsx
'use client';

import { useAuth } from '@clerk/nextjs';

export function ApiCall() {
  const { getToken } = useAuth();

  const fetchData = async () => {
    const token = await getToken();

    const response = await fetch('https://api.example.com/data', {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    return response.json();
  };

  return <button onClick={fetchData}>Fetch Data</button>;
}

Testing Authentication

After implementing authentication:

  1. Sign Up: Navigate to /sign-up and create a test account
  2. Email Verification: Check email for verification code (or use 424242 in dev mode)
  3. Sign In: Sign in at /sign-in with your credentials
  4. Protected Routes: Try accessing protected pages without auth
  5. Sign Out: Test sign-out functionality

In development, Clerk provides test mode. Use code 424242 for email verification or check your terminal for magic links.

Next Steps

How is this guide ?

Last updated on

On this page