CROP
ProjectsParts Services

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:

  1. Customer App (app.crop.com) - Public-facing application
  2. 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/nextjs

Clerk 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.com

Admin 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.com

IMPORTANT: 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

  1. Start backend services:

    cd /Users/vova/Code/CROP/microservices
    bun run dev
  2. Start frontend:

    cd CROP-front
    npm run dev
  3. Navigate to http://localhost:3000

  4. Sign up / Sign in

  5. 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, email

Test Admin Access

  1. Create user account
  2. Backend team sets role: admin in Clerk Dashboard
  3. Sign out and sign in again (to refresh token)
  4. Access /admin routes - should work
  5. 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_KEY in 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 ✅

  1. Always use { template: 'api' } when calling getToken()
  2. Never store tokens in localStorage (Clerk handles this securely)
  3. Use Server Components for sensitive data fetching when possible
  4. Validate roles on both frontend (UX) and backend (security)
  5. Use HTTPS in production (required for Clerk)

Don'ts ❌

  1. Don't use getToken() without template (uses session token, no explicit aud)
  2. Don't expose CLERK_SECRET_KEY to client (only use in Server Components/Actions)
  3. Don't trust client-side role checks alone (always validate on backend)
  4. Don't cache tokens manually (Clerk SDK handles caching)
  5. 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_KEY uses production key (pk_live_xxx)
  • CLERK_SECRET_KEY uses production key (sk_live_xxx)
  • NEXT_PUBLIC_API_URL points 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 methods
  • useUser() - Get current user data
  • useClerk() - Access Clerk instance for advanced operations
  • useSignIn() - Custom sign-in flow control
  • useSignUp() - Custom sign-up flow control

Key Clerk Functions (Server Components)

  • auth() - Get authentication state
  • currentUser() - Get full user object
  • clerkClient - 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_DOMAIN matches 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: Authorization header should be present
  • Verify backend logs for specific error (azp? aud? expired?)

Support & Resources


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

On this page