Skip to content

Clerk Integration Guide

This guide walks you through integrating Clerk with Centrali for external authentication and authorization.

Prerequisites

  • A Clerk account with an application
  • A Centrali workspace
  • The Centrali SDK installed (npm install @centrali-io/centrali-sdk)

Step 1: Create a JWT Template in Clerk

Clerk uses JWT Templates to customize the claims in your tokens. Create a template that includes the claims you want to use for authorization.

In Clerk Dashboard:

  1. Go to Configure → Sessions → JWT Templates
  2. Click Add new templateBlank
  3. Name it (e.g., centrali)

Clerk JWT Templates

Configure the Template:

Basic Settings: - Token Lifetime: 60-300 seconds recommended - Allowed Clock Skew: 5 seconds

Edit JWT Template

Claims:

Add the claims you want available in Centrali policies:

{
  "aud": "convex",
  "name": "{{user.full_name}}",
  "role": "{{user.public_metadata.role}}",
  "email": "{{user.primary_email_address}}",
  "plan": "{{user.public_metadata.plan}}",
  "org_id": "{{org.id}}",
  "org_role": "{{org.role}}"
}

Clerk Claims

Available Clerk Variables: - {{user.id}} - Clerk user ID - {{user.full_name}} - User's full name - {{user.primary_email_address}} - User's email - {{user.public_metadata.*}} - Custom user metadata - {{org.id}} - Organization ID - {{org.role}} - User's role in the organization


Step 2: Configure External Auth Provider in Centrali

In Centrali Console:

  1. Go to Settings → External Authentication Providers
  2. Click Add Provider

Add Provider

Fill in the Configuration:

Field Value
Provider Name Clerk (or any name)
Provider Type Clerk
Issuer URL https://<your-clerk-domain>.clerk.accounts.dev
Allowed Audiences Your audience value (e.g., convex)

Find your Clerk issuer URL: - In Clerk Dashboard → Configure → Sessions → JWT Templates - Click your template → Copy the Issuer field

Centrali Provider Settings

Configure Claim Mappings:

Map JWT claims to policy attributes:

[
  {
    "jwtPath": "aud",
    "attribute": "aud",
    "required": true
  },
  {
    "jwtPath": "name",
    "attribute": "name",
    "required": false
  },
  {
    "jwtPath": "role",
    "attribute": "role",
    "required": false
  },
  {
    "jwtPath": "plan",
    "attribute": "plan",
    "required": false,
    "defaultValue": "free"
  },
  {
    "jwtPath": "org_id",
    "attribute": "org_id",
    "required": false
  },
  {
    "jwtPath": "org_role",
    "attribute": "org_role",
    "required": false
  }
]

Claim Mappings

These claims become available in policies as: - ext_aud - ext_name - ext_role - ext_plan - ext_org_id - ext_org_role


Step 3: Create Authorization Policies

Create policies that use the extracted claims.

Example: Role-Based Access

{
  "name": "admin_access",
  "specification": {
    "rules": [{
      "rule_id": "admin-allow",
      "effect": "Allow",
      "conditions": [
        { "function": "string_equal", "attribute": "ext_role", "value": "admin" }
      ]
    }],
    "default": { "effect": "Deny" }
  }
}

Example: Plan-Based Feature Gating

{
  "name": "premium_features",
  "specification": {
    "rules": [{
      "rule_id": "premium-allow",
      "effect": "Allow",
      "conditions": [
        { "function": "string_one_of", "attribute": "ext_plan", "values": ["premium", "enterprise"] }
      ]
    }],
    "default": { "effect": "Deny" }
  }
}

Step 4: Integrate in Your Application

Get the Token from Clerk

// In a Next.js App Router component
import { auth } from '@clerk/nextjs/server';

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

  // Get token with your custom template
  const token = await getToken({ template: 'centrali' });

  // token is now a JWT with your custom claims
}

Check Authorization with Centrali

import { CentraliSDK } from '@centrali-io/centrali-sdk';

const centrali = new CentraliSDK({
  baseUrl: 'https://api.centrali.io',
  workspaceId: 'your-workspace',
});

export async function POST(request: Request) {
  const { getToken } = await auth();
  const token = await getToken({ template: 'centrali' });

  if (!token) {
    return Response.json({ error: 'Not authenticated' }, { status: 401 });
  }

  // Check authorization
  const result = await centrali.checkAuthorization({
    token,
    resource: 'premium-features',
    action: 'access',
  });

  if (!result.data.allowed) {
    return Response.json({ error: 'Access denied' }, { status: 403 });
  }

  // User has access - proceed
  return Response.json({ data: 'Premium content here' });
}

With Context for Dynamic Authorization

export async function POST(request: Request) {
  const { getToken } = await auth();
  const token = await getToken({ template: 'centrali' });
  const body = await request.json();

  const result = await centrali.checkAuthorization({
    token,
    resource: 'orders',
    action: 'approve',
    context: {
      orderId: body.orderId,
      orderAmount: body.amount,
    },
  });

  if (!result.data.allowed) {
    return Response.json({ error: 'Cannot approve this order' }, { status: 403 });
  }

  // Approve the order
  await approveOrder(body.orderId);
  return Response.json({ success: true });
}

Step 5: Setting User Metadata in Clerk

To use custom claims like plan or role, set them in Clerk's user metadata.

Via Clerk Dashboard:

  1. Go to Users → Select a user
  2. Click Edit metadata
  3. Add public metadata:
{
  "plan": "premium",
  "role": "admin"
}

Via Clerk API:

import { clerkClient } from '@clerk/nextjs/server';

// When user upgrades their plan
export async function upgradeToPremium(userId: string) {
  await clerkClient.users.updateUserMetadata(userId, {
    publicMetadata: {
      plan: 'premium',
    },
  });
}

Complete Example: Next.js API Route

// app/api/orders/[id]/approve/route.ts
import { auth } from '@clerk/nextjs/server';
import { CentraliSDK } from '@centrali-io/centrali-sdk';
import { NextRequest } from 'next/server';

const centrali = new CentraliSDK({
  baseUrl: process.env.CENTRALI_API_URL!,
  workspaceId: process.env.CENTRALI_WORKSPACE!,
});

export async function POST(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  // 1. Check authentication
  const { userId, getToken } = await auth();
  if (!userId) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // 2. Get order details
  const order = await getOrder(params.id);
  if (!order) {
    return Response.json({ error: 'Order not found' }, { status: 404 });
  }

  // 3. Get Clerk token with custom template
  const token = await getToken({ template: 'centrali' });
  if (!token) {
    return Response.json({ error: 'Failed to get token' }, { status: 500 });
  }

  // 4. Check authorization with Centrali
  const authResult = await centrali.checkAuthorization({
    token,
    resource: 'orders',
    action: 'approve',
    context: {
      orderId: params.id,
      orderAmount: order.total,
      department: order.department,
    },
  });

  if (!authResult.data.allowed) {
    return Response.json(
      { error: 'Not authorized to approve this order' },
      { status: 403 }
    );
  }

  // 5. Perform the action
  await approveOrder(params.id);

  return Response.json({ success: true, orderId: params.id });
}

Debugging: View Token Claims

Create a debug page to see your JWT claims during development:

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

function decodeJWT(token: string) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    return JSON.parse(atob(parts[1]));
  } catch {
    return null;
  }
}

export default async function DebugPage() {
  const { userId, getToken } = await auth();

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

  const token = await getToken({ template: 'centrali' });
  const claims = token ? decodeJWT(token) : null;

  return (
    <div className="p-8 space-y-8">
      <h1 className="text-2xl font-bold">JWT Debug</h1>

      <div className="bg-gray-100 p-4 rounded">
        <h2 className="font-semibold mb-2">Token</h2>
        <code className="text-sm break-all">{token || 'No token'}</code>
      </div>

      <div className="bg-gray-100 p-4 rounded">
        <h2 className="font-semibold mb-2">Claims</h2>
        <pre className="text-sm">
          {claims ? JSON.stringify(claims, null, 2) : 'No claims'}
        </pre>
      </div>
    </div>
  );
}

Troubleshooting

"Unknown issuer" Error

Cause: JWT issuer doesn't match the registered provider.

Solution: 1. In Clerk Dashboard, go to your JWT Template 2. Copy the Issuer URL exactly 3. Update your Centrali provider's Issuer URL

Token Validation Failed

Cause: JWKS signature verification failed.

Solution: 1. Verify Clerk's JWKS endpoint is accessible 2. Check token hasn't expired 3. Ensure audience matches allowed audiences

Claims Not Appearing

Cause: Claim mappings don't match JWT structure.

Solution: 1. Use the debug page to see actual JWT claims 2. Verify jwtPath matches exactly 3. Check if claims are in public_metadata (requires {{user.public_metadata.field}})

"Access Denied" with Correct Claims

Cause: Policy conditions not matching.

Solution: 1. Check attribute values are exact (case-sensitive) 2. Verify you're using ext_ prefix in policies 3. Check policy is attached to the correct resource