Clerk Frontend Integration Guide
Prerequisites: - Backend Phases 1 & 2 must be deployed - Phase 0 (Clerk Dashboard setup) must be completed - Environment variables from backend team
Clerk Frontend Integration Guide
For Frontend Team
Prerequisites:
- Backend Phases 1 & 2 must be deployed
- Phase 0 (Clerk Dashboard setup) must be completed
- Environment variables from backend team
Overview
This guide covers integrating Clerk authentication into the Next.js frontend applications:
- Customer App (
app.crop.com) - Public-facing application - Admin Dashboard (
admin.crop.com) - Internal admin interface
Both apps share the same Clerk application but run on different domains (satellite domain setup).
Installation
Install Clerk SDK
# Customer app
cd CROP-front
npm install @clerk/nextjs
# Admin app
cd CROP-front-admin
npm install @clerk/nextjsClerk SDK Version
Minimum version: @clerk/nextjs@5.0.0 (supports App Router and satellite domains)
Environment Variables
Customer App (CROP-front/.env.local)
# Clerk Configuration
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
# API Configuration
NEXT_PUBLIC_API_URL=https://api.crop.com
# Clerk Domain (Primary)
NEXT_PUBLIC_CLERK_DOMAIN=app.crop.comAdmin App (CROP-front-admin/.env.local)
# Clerk Configuration (SAME keys as customer app)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
# API Configuration
NEXT_PUBLIC_API_URL=https://api.crop.com
# Clerk Domain (Satellite)
NEXT_PUBLIC_CLERK_DOMAIN=admin.crop.comIMPORTANT: Both apps use the same Clerk keys because they share the same Clerk application. The domain difference is handled automatically by Clerk's satellite domain feature.
Basic Setup
1. Wrap App with ClerkProvider
app/layout.tsx:
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}2. Create Middleware for Auth
middleware.ts:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
'/api/webhooks(.*)', // Allow webhook endpoints
]);
export default clerkMiddleware((auth, request) => {
if (!isPublicRoute(request)) {
auth().protect();
}
});
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
],
};3. Configure Sign-In/Sign-Up Pages
Create: 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-lg',
},
}}
routing="path"
path="/sign-in"
signUpUrl="/sign-up"
/>
</div>
);
}Create: 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
appearance={{
elements: {
rootBox: 'mx-auto',
card: 'shadow-lg',
},
}}
routing="path"
path="/sign-up"
signInUrl="/sign-in"
/>
</div>
);
}API Integration
Fetch API with Clerk Token
CRITICAL: Use the api JWT template (configured in Phase 0).
lib/api.ts:
import { auth } from '@clerk/nextjs/server';
/**
* Server-side API fetch with Clerk authentication
* Use this in Server Components and Server Actions
*/
export async function fetchAPI<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const { getToken } = auth();
// IMPORTANT: Use template 'api' for explicit aud validation
const token = await getToken({ template: 'api' });
if (!token) {
throw new Error('Unauthorized: No token available');
}
const url = `${process.env.NEXT_PUBLIC_API_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || `API Error: ${response.status}`);
}
return response.json();
}lib/api-client.ts (for Client Components):
'use client';
import { useAuth } from '@clerk/nextjs';
/**
* Client-side API fetch hook
* Use this in Client Components
*/
export function useAPI() {
const { getToken } = useAuth();
const fetchAPI = async <T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> => {
const token = await getToken({ template: 'api' });
if (!token) {
throw new Error('Unauthorized: No token available');
}
const url = `${process.env.NEXT_PUBLIC_API_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || `API Error: ${response.status}`);
}
return response.json();
};
return { fetchAPI };
}Usage Examples
Server Component:
import { fetchAPI } from '@/lib/api';
export default async function ProfilePage() {
const profile = await fetchAPI('/api/users/me');
return (
<div>
<h1>Profile</h1>
<p>Email: {profile.email}</p>
<p>Role: {profile.role}</p>
</div>
);
}Client Component:
'use client';
import { useAPI } from '@/lib/api-client';
import { useEffect, useState } from 'react';
export function Dashboard() {
const { fetchAPI } = useAPI();
const [data, setData] = useState(null);
useEffect(() => {
fetchAPI('/api/dashboard')
.then(setData)
.catch(console.error);
}, [fetchAPI]);
return <div>{/* Render data */}</div>;
}Server Action:
'use server';
import { fetchAPI } from '@/lib/api';
export async function updateProfile(formData: FormData) {
const name = formData.get('name') as string;
await fetchAPI('/api/users/me', {
method: 'PATCH',
body: JSON.stringify({ name }),
});
return { success: true };
}Role-Based Access Control
Admin Dashboard Only
app/admin/layout.tsx:
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const { sessionClaims } = auth();
// Check if user has admin role
const role = sessionClaims?.metadata?.role as string | undefined;
if (role !== 'admin') {
redirect('/'); // Redirect non-admins
}
return <div className="admin-layout">{children}</div>;
}Client-Side Role Check
'use client';
import { useAuth } from '@clerk/nextjs';
export function AdminOnlyButton() {
const { sessionClaims } = useAuth();
const role = sessionClaims?.metadata?.role as string | undefined;
if (role !== 'admin') {
return null;
}
return <button>Admin Action</button>;
}Middleware-Based Protection
middleware.ts:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
const isAdminRoute = createRouteMatcher(['/admin(.*)']);
export default clerkMiddleware((auth, request) => {
if (isAdminRoute(request)) {
const { sessionClaims } = auth();
const role = sessionClaims?.metadata?.role as string | undefined;
if (role !== 'admin') {
const url = new URL('/', request.url);
return NextResponse.redirect(url);
}
}
});User Profile & Management
User Button Component
import { UserButton } from '@clerk/nextjs';
export function Header() {
return (
<header>
<nav>
{/* ... */}
<UserButton
appearance={{
elements: {
avatarBox: 'w-10 h-10',
},
}}
afterSignOutUrl="/"
/>
</nav>
</header>
);
}Get Current User
Server Component:
import { currentUser } from '@clerk/nextjs/server';
export default async function ProfilePage() {
const user = await currentUser();
return (
<div>
<h1>{user?.firstName} {user?.lastName}</h1>
<p>{user?.emailAddresses[0]?.emailAddress}</p>
<p>Role: {user?.privateMetadata?.role as string}</p>
</div>
);
}Client Component:
'use client';
import { useUser } from '@clerk/nextjs';
export function Profile() {
const { user, isLoaded } = useUser();
if (!isLoaded) return <div>Loading...</div>;
return (
<div>
<h1>{user?.firstName} {user?.lastName}</h1>
<p>{user?.emailAddresses[0]?.emailAddress}</p>
</div>
);
}Advanced Features
Custom Sign-Up Flow
With additional fields:
import { SignUp } from '@clerk/nextjs';
export default function SignUpPage() {
return (
<SignUp
appearance={{ /* ... */ }}
unsafeMetadata={{
onboarding_completed: false,
}}
afterSignUpUrl="/onboarding"
/>
);
}Redirect After Sign-In
<SignIn
afterSignInUrl="/dashboard"
afterSignUpUrl="/onboarding"
/>Organization Support (Future)
If you need multi-tenancy later:
import { OrganizationSwitcher } from '@clerk/nextjs';
<OrganizationSwitcher
appearance={{ /* ... */ }}
afterCreateOrganizationUrl="/org/:slug"
/>Testing
Local Development
-
Start backend services:
cd /Users/vova/Code/CROP/microservices bun run dev -
Start frontend:
cd CROP-front npm run dev -
Navigate to
http://localhost:3000 -
Sign up / Sign in
-
Check Network tab: API calls should have
Authorization: Bearer <token>
Test API Authentication
// In browser console (after sign-in)
const { getToken } = window.Clerk;
const token = await getToken({ template: 'api' });
console.log(token);
// Decode JWT at https://jwt.io
// Verify claims: aud, azp, role, emailTest Admin Access
- Create user account
- Backend team sets
role: adminin Clerk Dashboard - Sign out and sign in again (to refresh token)
- Access
/adminroutes - should work - Create another user (without admin role) - should redirect from
/admin
Error Handling
Common Errors
"aud mismatch"
- Cause: JWT template not configured or wrong audience
- Fix: Verify Phase 0 setup, JWT template should have
"aud": "https://api.crop.com"
"azp mismatch"
- Cause: Frontend domain not in authorized parties
- Fix: Backend should have
CLERK_AUTHORIZED_PARTIES=https://app.crop.com,https://admin.crop.com
"No token available"
- Cause: User not signed in or session expired
- Fix: Redirect to sign-in page
"Invalid token"
- Cause: Backend can't verify token signature
- Fix: Check
CLERK_JWT_KEYin backend, ensure it matches Clerk Dashboard
Error Boundary
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex min-h-screen items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold">Something went wrong!</h2>
<p className="text-gray-600">{error.message}</p>
<button
onClick={() => reset()}
className="mt-4 rounded bg-blue-500 px-4 py-2 text-white"
>
Try again
</button>
</div>
</div>
);
}Security Best Practices
Do's ✅
- Always use
{ template: 'api' }when callinggetToken() - Never store tokens in localStorage (Clerk handles this securely)
- Use Server Components for sensitive data fetching when possible
- Validate roles on both frontend (UX) and backend (security)
- Use HTTPS in production (required for Clerk)
Don'ts ❌
- Don't use
getToken()without template (uses session token, no explicitaud) - Don't expose
CLERK_SECRET_KEYto client (only use in Server Components/Actions) - Don't trust client-side role checks alone (always validate on backend)
- Don't cache tokens manually (Clerk SDK handles caching)
- Don't share tokens between domains (each domain gets its own, even with satellite setup)
Deployment Checklist
Before deploying to production:
- Environment variables added to Vercel/deployment platform
-
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYuses production key (pk_live_xxx) -
CLERK_SECRET_KEYuses production key (sk_live_xxx) -
NEXT_PUBLIC_API_URLpoints to production API - Middleware configured to protect routes
- Role checks implemented for admin routes
- Error handling for API failures
- Sign-in/Sign-up pages styled and tested
- Cross-domain session tested (app.crop.com ↔ admin.crop.com)
- Backend is deployed and webhook endpoint is live
API Reference
Key Clerk Hooks (Client Components)
useAuth()- Get authentication state and methodsuseUser()- Get current user datauseClerk()- Access Clerk instance for advanced operationsuseSignIn()- Custom sign-in flow controluseSignUp()- Custom sign-up flow control
Key Clerk Functions (Server Components)
auth()- Get authentication statecurrentUser()- Get full user objectclerkClient- Interact with Clerk API
Token Claims Structure
When you call getToken({ template: 'api' }), you get a JWT with these claims:
{
"aud": "https://api.crop.com",
"azp": "https://app.crop.com",
"sub": "user_2xxx",
"iss": "https://clerk.crop.com",
"email": "user@example.com",
"role": "customer",
"name": "John Doe",
"exp": 1699999999,
"iat": 1699996399,
"nbf": 1699996399
}Troubleshooting
Infinite Redirect Loop
Cause: Middleware protecting sign-in routes
Fix: Add sign-in routes to public routes:
const isPublicRoute = createRouteMatcher([
'/',
'/sign-in(.*)',
'/sign-up(.*)',
]);Session Not Persisting
Cause: Cookies blocked or HTTPS issue
Fix:
- Ensure HTTPS in production
- Check browser cookie settings
- Verify
NEXT_PUBLIC_CLERK_DOMAINmatches actual domain
API Returns 401 After Sign-In
Cause: Token not included or wrong template used
Fix:
- Ensure
getToken({ template: 'api' })is used - Check Network tab:
Authorizationheader should be present - Verify backend logs for specific error (azp? aud? expired?)
Support & Resources
- Clerk Documentation: https://clerk.com/docs/quickstarts/nextjs
- Clerk Discord: https://clerk.com/discord
- Backend Team: Vova (@vova-appdev)
- Implementation Plan:
docs/CLERK_IMPLEMENTATION_PLAN_FINAL.md - Phase 0 Setup:
docs/CLERK_PHASE0_MANUAL_SETUP.md
Example Project Structure
CROP-front/
├── app/
│ ├── (auth)/
│ │ ├── sign-in/[[...sign-in]]/page.tsx
│ │ └── sign-up/[[...sign-up]]/page.tsx
│ ├── (protected)/
│ │ ├── dashboard/page.tsx
│ │ ├── profile/page.tsx
│ │ └── admin/
│ │ ├── layout.tsx (role check)
│ │ └── page.tsx
│ ├── layout.tsx (ClerkProvider)
│ └── page.tsx
├── lib/
│ ├── api.ts (server-side fetch)
│ └── api-client.ts (client-side hook)
├── middleware.ts (auth + role protection)
└── .env.local (Clerk keys)Last updated: 2025-11-13
For backend integration details, see CLERK_IMPLEMENTATION_PLAN_FINAL.md