Clerk

This guide walks you through integrating Clerk authentication with Strapi v5, covering middleware setup, JWT verification, webhook configuration, and user synchronization to build secure, content-driven applications

Clerk

What Is Clerk?

Clerk is a developer-focused authentication platform that handles user management, session handling, and authentication flows for modern web applications. Rather than building authentication infrastructure from scratch, developers can use Clerk's prebuilt UI components, SDKs, and APIs to add secure sign-in functionality.

The platform supports multiple authentication methods out of the box: email verification codes, magic links, phone authentication via SMS, passkeys, social login through OAuth providers, and traditional username/password combinations. Clerk also provides enterprise features like Single Sign-On (SSO) and Multi-Factor Authentication (MFA) without requiring custom development.

For backend integrations, Clerk issues short-lived JWT session tokens that can be verified server-side. This token-based approach fits well with headless CMS architectures where authentication happens on the client while content APIs run separately.

Why Integrate Clerk with Strapi

Combining Clerk's authentication capabilities with Strapi's content management features creates a powerful foundation for building secure, content-driven applications.

Here's what this integration enables:

  • Offloaded authentication complexity: Clerk handles sign-in flows, password management, and session security entirely, letting Strapi focus on content management and API delivery
  • Automatic user synchronization: When users sign up through Clerk, webhook events create corresponding records in Strapi's users-permissions system automatically
  • Native RBAC compatibility: Strapi's role-based access control continues working normally—you assign roles to Clerk-synced users just like any other user
  • Modern authentication methods: Social login, passwordless authentication, and MFA become available without custom development work
  • Reduced security surface: Concentrating authentication in Clerk's managed infrastructure eliminates vulnerabilities associated with self-managed JWT systems
  • Improved developer experience: Pre-built SDKs and UI components mean less code to write, test, and maintain

How to Integrate Clerk with Strapi

This integration uses the strapi-plugin-clerk-auth approach, which provides the most complete solution with built-in JWT verification using Clerk's secret key via the @clerk/backend SDK, user synchronization via webhooks with Svix signature verification, and route protection middleware including both JWT authentication and user ownership enforcement.

Prerequisites

Before starting, confirm your environment meets these requirements:

  • Node.js version: Strapi v5 requires Node.js v20, v22, or v24 (LTS versions only). Clerk requires Node.js 18.17.0 or higher. Use Node.js v20+ to satisfy both requirements.
  • Database: Strapi v5 supports MySQL 8.0+, MariaDB 10.5+, PostgreSQL 12.0+, or SQLite 3+.
  • Accounts and API keys:
    • A Clerk account with an application configured
    • Clerk Publishable Key and Secret Key from your Clerk Dashboard
    • Webhook signing secret (you'll create this during setup)
  • Strapi v5 project: If you don't have one yet, create it with:
npx create-strapi@latest my-project --quickstart

Step 1: Install the Clerk Auth Plugin

Navigate to your Strapi project's plugins directory and clone the authentication plugin:

cd src/plugins
git clone https://github.com/PaulBratslavsky/strapi-plugin-clerk-auth.git clerk-auth
cd clerk-auth
npm install

The plugin installs two critical dependencies:

  • @clerk/backend for JWT verification using Clerk's SDK
  • svix for webhook signature verification

Step 2: Register the Plugin

Add the plugin to your Strapi configuration. Create or update config/plugins.ts:

export default {
  'clerk-auth': {
    enabled: true,
    resolve: './src/plugins/clerk-auth'
  },
};

This registers the Clerk authentication plugin in your Strapi v5 project, enabling JWT verification middleware and user synchronization functionality.

Step 3: Configure Environment Variables

Add your Clerk credentials to the .env file at your project root:

CLERK_SECRET_KEY=sk_test_your_secret_key_here
CLERK_WEBHOOK_SECRET=whsec_your_webhook_secret_here

You can find your Secret Key in the Clerk Dashboard under API Keys. The webhook secret comes later when you configure the webhook endpoint.

Step 4: Configure the Webhook Endpoint

Webhooks keep Strapi's user database in sync with Clerk user events. When someone signs up, updates their profile, or deletes their account in Clerk, webhooks notify Strapi to create, update, or delete the corresponding user record in Strapi's users-permissions table. The webhook handler verifies signatures using the Svix library and stores the clerkId and user information from Clerk events.

The plugin exposes a webhook endpoint at /api/clerk-auth/webhook. Configure it in your Clerk Dashboard:

  1. Navigate to the Webhooks section
  2. Click "Add Endpoint"
  3. Enter your Strapi URL: https://your-strapi-domain.com/api/clerk-auth/webhook
  4. Subscribe to these events: user.created, user.updated, user.deleted
  5. Copy the Signing Secret and add it to your .env file as CLERK_WEBHOOK_SECRET

For local development, use a tunneling service like ngrok to expose your local Strapi instance:

ngrok http 1337

Then use the ngrok URL as your webhook endpoint.

Step 5: Protect API Routes

The plugin provides two middleware functions for route protection: clerk-auth middleware for JWT verification and user authentication, and is-user-owner middleware for ownership-based access control.

  • clerk-auth: Extracts and verifies the JWT token using Clerk's secret key, queries Strapi's users-permissions table for a matching clerkId, creates a new user record if none exists, and attaches the authenticated user to the request context.
  • is-user-owner: Enforces ownership validation by comparing the authenticated user's ID against resource ownership to prevent unauthorized access to user-specific resources.

Apply these middlewares to routes that require authentication. Create or modify route files in src/api/[apiName]/routes/:

export default {
  routes: [
    {
      method: 'GET',
      path: '/users/me',
      handler: 'user.me',
      config: {
        middlewares: ['plugin::clerk-auth.clerk-auth'],
        auth: false,
      },
    },
    {
      method: 'PUT',
      path: '/users/:id',
      handler: 'user.update',
      config: {
        middlewares: [
          'plugin::clerk-auth.clerk-auth',
          'plugin::clerk-auth.is-user-owner'
        ],
        auth: false,
      },
    },
  ],
};

Setting auth: false is critical—it prevents conflicts between Clerk's JWT verification and Strapi's built-in authentication system.

Step 6: Send Authenticated Requests from Your Frontend

Your frontend application needs to obtain tokens from Clerk and include them in API requests to Strapi. Here's how that looks:

// Get the session token from Clerk
const token = await clerk.session.getToken();

// Include it in requests to your Strapi API
const response = await fetch('https://your-strapi-api.com/api/users/me', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
});

const userData = await response.json();

The Clerk SDKs for React, Next.js, and other frameworks provide hooks and components that simplify token retrieval and authentication integration with Strapi.

Step 7: Verify the Integration

Rebuild and restart your Strapi server:

npm run build
npm run develop

Test the integration by:

  1. Signing in through your Clerk-powered frontend
  2. Making an authenticated request to a protected Strapi endpoint
  3. Checking the Strapi Admin Panel to confirm the user was created

Project Example: Building a Membership Content Platform

A membership content platform demonstrates how Clerk and Strapi work together in practice. Users authenticate through Clerk to access premium articles, courses, or resources managed in Strapi.

Architecture Overview

The system has three main components:

  1. Frontend application: Handles user authentication through Clerk's UI components
  2. Clerk: Manages authentication, issues JWTs, and sends webhook events
  3. Strapi: Stores content, manages user records, and enforces access control through RBAC

When a user signs up, Clerk's webhook notifies Strapi to create a user record. The frontend then requests protected content by including the Clerk JWT in API calls. Strapi's middleware verifies the token and checks permissions before returning content.

Content Model Setup

Create a content type for membership content in Strapi. Using the Content-Type Builder, define an Article type with these fields:

  • title (Text)
  • content (Rich Text)
  • tier (Enumeration: free, basic, premium)
  • author (Relation to User)

Protected Route Implementation

Create a custom controller that filters content based on user membership tier:

// src/api/article/controllers/article.ts
export default {
  async findAccessible(ctx) {
    const user = ctx.state.user;
    
    if (!user) {
      // Return only free content for unauthenticated users
      return await strapi.documents('api::article.article').findMany({
        filters: { tier: 'free' },
        populate: ['author'],
      });
    }

    // Get user's membership tier from metadata
    const userTier = user.membershipTier || 'free';
    
    // Define accessible tiers based on membership level
    const accessibleTiers = {
      free: ['free'],
      basic: ['free', 'basic'],
      premium: ['free', 'basic', 'premium'],
    };

    return await strapi.documents('api::article.article').findMany({
      filters: { 
        tier: { $in: accessibleTiers[userTier] } 
      },
      populate: ['author'],
    });
  },
};

Route Configuration

Register the protected route with Clerk authentication middleware:

// src/api/article/routes/custom.ts
// Protected route for accessing public articles without Clerk authentication
export default {
  routes: [
    {
      method: 'GET',
      path: '/articles/accessible',
      handler: 'article.findAccessible',
      config: {
        // Apply Clerk JWT verification middleware from the strapi-plugin-clerk-auth
        middlewares: ['plugin::clerk-auth.clerk-auth'],
        // Disable Strapi's built-in authentication to prevent JWT validation conflicts
        // Clerk's custom middleware handles token verification instead
        auth: false,
      },
    },
  ],
};

User Tier Synchronization

Extend the webhook handler to sync membership tier data from Clerk's user metadata:

// In your webhook handler
case 'user.updated':
  const existingUser = await strapi.db
    .query('plugin::users-permissions.user')
    .findOne({ where: { clerkId: evt.data.id } });
    
  if (existingUser) {
    await strapi.db.query('plugin::users-permissions.user').update({
      where: { id: existingUser.id },
      data: {
        clerkId: evt.data.id,
        fullName: `${evt.data.first_name} ${evt.data.last_name}`,
        // Optional: map custom metadata from Clerk public_metadata
        ...(evt.data.public_metadata?.tier && { membershipTier: evt.data.public_metadata.tier }),
      },
    });
  }
  break;

Frontend Integration

On the frontend, fetch accessible content using the authenticated user's token:

import { useAuth } from '@clerk/clerk-react';

function ArticleList() {
  const { getToken } = useAuth();
  const [articles, setArticles] = useState([]);

  useEffect(() => {
    async function fetchArticles() {
      const token = await getToken();
      
      const response = await fetch(
        `${process.env.STRAPI_URL}/api/articles/accessible`,
        {
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json',
          },
        }
      );
      
      const data = await response.json();
      setArticles(data);
    }
    
    fetchArticles();
  }, [getToken]);

  return (
    <div>
      {articles.map((article) => (
        <article key={article.id}>
          <h2>{article.title}</h2>
          <span>{article.tier}</span>
        </article>
      ))}
    </div>
  );
}

This pattern scales to other content-driven applications: e-commerce platforms with protected checkout, blogs with subscriber-only content, or SaaS applications with tiered feature access.

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and Clerk documentation.

Frequently Answered Questions

CORS errors occur when your frontend domain isn't allowed to make requests to your Strapi API. Configure the strapi::cors middleware in config/middlewares.js by adding your frontend domain to the origin array and including Authorization in the headers array. Set credentials: true if using cookies. Avoid using wildcard (*) origins when credentials are enabled—this combination is blocked by browsers. Centralize your CORS configuration in one location to prevent duplicate header conflicts.

Strapi's default JWT validation expects tokens in its own format, not Clerk's. The solution is using custom middleware with Clerk's @clerk/backend SDK to verify tokens instead of Strapi's built-in validation. To troubleshoot token validation issues, confirm your Authorization header follows the Bearer <token> format exactly, check that your CLERK_SECRET_KEY environment variable is set correctly, and verify the token hasn't expired by checking Clerk's session logs. Setting auth: false on protected routes is critical—it disables Strapi's default authentication system to prevent middleware conflicts between Clerk's JWT verification and Strapi's built-in validation.

Configure webhooks in Clerk Dashboard by navigating to Webhooks, clicking 'Add Endpoint,' and entering your Strapi webhook URL: https://your-domain.com/api/clerk-auth/webhook. Subscribe to user.created, user.updated, and user.deleted events. Copy the signing secret to your .env file as CLERK_WEBHOOK_SECRET. For local development, use ngrok to create a public tunnel to your local Strapi instance. The plugin's Svix integration automatically verifies webhook signatures for security.

Yes, you can use Strapi's role-based access control (RBAC) system with Clerk authentication. The integration provides seamless user synchronization between Clerk and Strapi's role-based access control system. When Strapi receives a verified Clerk JWT, it automatically maps the Clerk user to the Strapi user by email or creates new users automatically in Strapi's users-permissions table.

Strapi's permission system functions normally without additional authorization logic. According to Strapi's documentation on external JWT authentication, developers can successfully override Strapi's default authentication to accept Clerk's external JWTs while maintaining the users-permissions plugin functionality. The is-user-owner middleware enforces ownership validation by comparing the authenticated user's ID against resource ownership, preventing unauthorized access to user-specific resources while respecting role-based permissions.

Additionally, the integration uses webhook-driven updates to keep user data synchronized in real-time, with webhook security ensuring data integrity through cryptographic signature verification. This allows you to manage roles and permissions through Strapi's native RBAC system while using Clerk for centralized authentication management.

The plugin approach provides a complete solution with built-in JWT verification, user synchronization via webhooks, ownership checks, and route protection—ideal for getting to production quickly. Custom middleware gives you full control over the authentication flow but requires more code and ongoing maintenance. Start with the plugin unless you have specific requirements it doesn't address. You can always extend the plugin or migrate to custom middleware later as your needs evolve.