Docs

Backend-only SDK

When creating a backend-only SDK, you have two options for implementing the BAPI endpoints: either develop a backend SDK that encompasses all BAPI endpoints or create an SDK tailored for an existing backend framework.

The source of truth for all BAPI endpoints is the BAPI reference docs. For Node.js backend frameworks, you can build on top of the JavaScript Backend SDK.

Expected features

  • User only needs to provide their Secret Key
  • Centralized request authentication (e.g. in a middleware or plugin)
  • Give access to the instance of BAPI client (so that users can use all methods)
  • User should be able to limit access to routes by checking for roles and permissions

Important

BAPI has rate limits to help protect users against brute-force attacks or stop abuse of Clerk's platform. Be sure to include a backoff mechanism into your fetching logic and respect the Retry-After header to gracefully handle any active rate limits.

If you're using @clerk/backend to build an SDK for an existing framework, these additional features are expected:

  • User should be able to use all @clerk/backend options

Optional features

  • User should be able to enforce authentication on individual routes (e.g. with a requireAuth helper)
  • Use singleton pattern to only create a pre-configured instance of Clerk backend client

Implementation: BAPI

You can manually create a wrapper library around the BAPI OpenAPI or use one the many automatic SDK generation tools that take in OpenAPI definitions.

Note

If you're looking for a real-world example, have a look at clerk-sdk-go.

Implementation: Node.js backend framework

@clerk/backend is built for Node.js/V8 isolates (Cloudflare Workers, Vercel Edge Runtime, etc.). It’s the foundational package for all JavaScript Backend SDKs and works across all JavaScript runtimes. By using @clerk/backend you can be sure to communicate with Clerk’s BAPI in a correct and secure way.

Note

The code blocks below will be written in pseudo-code. If you're looking for real-world examples, have a look at @clerk/fastify and @clerk/express.

Create a Clerk client

Use createClerkClient from @clerk/backend to create your default Clerk client which will be used for the middleware.

client.ts
import { createClerkClient } from '@clerk/backend'

const API_VERSION = process.env.CLERK_API_VERSION || 'v1'
const SECRET_KEY = process.env.CLERK_SECRET_KEY || ''
const PUBLISHABLE_KEY = process.env.CLERK_PUBLISHABLE_KEY || ''
const API_URL = process.env.CLERK_API_URL || ''
const JWT_KEY = process.env.CLERK_JWT_KEY || ''
const SDK_METADATA = {
  name: PACKAGE_NAME,
  version: PACKAGE_VERSION,
  environment: process.env.NODE_ENV,
}

export const clerkClient = createClerkClient({
  secretKey: SECRET_KEY,
  apiUrl: API_URL,
  apiVersion: API_VERSION,
  jwtKey: JWT_KEY,
  userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
  sdkMetadata: SDK_METADATA,
})

Create your middleware/plugin

Inside the middleware, you’ll use the user-provided Clerk client (or use the default one created in the previous step) and authenticate the request. authenticateRequest returns Promise<RequestState>. The middleware should set requestState.toAuth() into its context as this will contain the resolved signed-in/signed-out Auth object. This way other helpers can access it later in the chain.

clerk-middleware.ts
import { clerkClient as defaultClerkClient } from './client.ts'

const clerkMiddleware = (options) => {
  return async (context, next) => {
    const clerkClient = options.clerkClient || defaultClerkClient

    const requestState = await clerkClient.authenticateRequest(context.req)

    context.set('clerkAuth', requestState.toAuth())
    context.set('clerk', clerkClient)

    await next()
  }
}

Create a getAuth helper

This utility will access the stored requestState (in the example above saved as clerkAuth) and return it.

get-auth.ts
export const getAuth = (context) => context.get('clerkAuth')

Your end-users can use this utility in cases like these:

index.ts
const app = new Framework()

app.use('*', clerkMiddleware())
app.get('/', (context) => {
  const auth = getAuth(context)

  if (!auth?.userId) {
    return context.json({ message: 'Not logged in' })
  }

  return context.json({ message: 'Logged in', userId: auth.userId })
})

Tip

See the Next.js getAuth() reference to see how it's implemented.

Create a requireAuth helper

This utility will require auth requests for user authenticated or authorized requests. An HTTP 401 status code is returned for unauthorized requests.

require-auth.ts
export const requireAuth = (context, next) => {
  if (!hasAuthObject(context)) {
    throw new Error('Middleware required')
  }
  if (!getAuth(context).userId) {
    context.status = 401
    return
  }
  return next()
}

Your end-users can use this utility in cases like these:

index.ts
const app = new Framework()

app.get('/path', requireAuth())

Your end-users will also have access to a has() function on the Auth object. They can combine it with requireAuth() like so:

index.ts
const app = new Framework()

const hasPermission = (context, next) => {
  const auth = getAuth(context)
  if (!auth.has({ permission: 'permission' })) {
    context.status = 403
    return
  }
  return next()
}

app.get('/path', requireAuth(), hasPermission())

Feedback

What did you think of this content?

Last updated on