Search docs

Jump between documentation pages.

Protect a DaloyJS API with Clerk

Clerk is a developer-first auth platform that bundles user management, organizations, billing, and embeddable UI components. For a backend API, the @clerk/backend package exposes authenticateRequest(), which takes a standard Request and returns an Authobject — a perfect fit for DaloyJS's Web-standard handlers.

1. Set up your Clerk app

  1. Create an application in the Clerk dashboard. From API Keys, copy the Publishable Key and Secret Key. Optionally copy the JWT Public Key (PEM) for networkless verification.
  2. Your frontend (Clerk's React, Next.js, Expo, or vanilla JS SDK) obtains a session token via getToken() and sends it in the Authorization: Bearer <token> header to your DaloyJS API.
  3. For machine-to-machine calls, create an M2M tokenor use Clerk's OAuth applications and accept oauth_token / m2m_token in the verifier.

2. Install

ts
pnpm add @clerk/backend

3. Environment variables

ts
# .env
CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLERK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Optional — enables networkless JWT verification (no Clerk API call per request)
# Get it from API Keys → Show JWT public key → PEM Public Key
CLERK_JWT_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"

4. Plugin

ts
// src/plugins/clerk.ts
import { createClerkClient } from "@clerk/backend";
import type { App, Middleware } from "@daloyjs/core";

const clerk = createClerkClient({
  secretKey: process.env.CLERK_SECRET_KEY!,
  publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
  jwtKey: process.env.CLERK_JWT_KEY, // optional: enables networkless verification
});

export interface Principal {
  userId: string;
  sessionId: string | null;
  orgId: string | null;
  orgRole: string | null;
  tokenType: string;
}

export const clerkPlugin = {
  name: "clerk",
  register(app: App) {
    app.decorate("clerk", clerk);
  },
};

export function requireClerkAuth(opts?: {
  acceptsToken?: "session_token" | "oauth_token" | "m2m_token" | "api_key" | "any";
  authorizedParties?: string[];
}): Middleware {
  const acceptsToken = opts?.acceptsToken ?? "session_token";
  return async (ctx, next) => {
    const result = await ctx.state.clerk.authenticateRequest(ctx.request, {
      acceptsToken,
      authorizedParties: opts?.authorizedParties,
    });
    if (!result.isAuthenticated) {
      return ctx.problem(401, "unauthorized", result.message ?? "Unauthorized");
    }
    const auth = result.toAuth();
    ctx.state.principal = {
      userId: (auth as { userId?: string }).userId ?? "",
      sessionId: (auth as { sessionId?: string | null }).sessionId ?? null,
      orgId: (auth as { orgId?: string | null }).orgId ?? null,
      orgRole: (auth as { orgRole?: string | null }).orgRole ?? null,
      tokenType: result.tokenType,
    };
    return next();
  };
}

declare module "@daloyjs/core" {
  interface AppState {
    clerk: ReturnType<typeof createClerkClient>;
    principal?: Principal;
  }
}

Setting authorizedPartiesis strongly recommended — it pins the origins allowed to make requests and protects against the subdomain-cookie-leaking attack described in Clerk's docs. Setting jwtKey turns verification into a pure crypto check (no network), which is ideal for edge runtimes.

5. Guard a route

ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { clerkPlugin, requireClerkAuth } from "./plugins/clerk";

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
app.register(clerkPlugin);

app.route({
  method: "GET",
  path: "/me",
  operationId: "getMe",
  middleware: [requireClerkAuth({ authorizedParties: ["https://acme.example.com"] })],
  responses: {
    200: {
      description: "OK",
      body: z.object({
        userId: z.string(),
        orgId: z.string().nullable(),
        orgRole: z.string().nullable(),
      }),
    },
  },
  handler: ({ state }) => ({
    status: 200,
    body: {
      userId: state.principal!.userId,
      orgId: state.principal!.orgId,
      orgRole: state.principal!.orgRole,
    },
  }),
});

Organizations & role checks

Clerk's Auth object includes the active orgId, orgSlug, orgRole (e.g. org:admin), and orgPermissions. Add a thin helper to require a role on top of requireClerkAuth:

ts
export function requireOrgRole(role: string): Middleware {
  return async (ctx, next) => {
    if (ctx.state.principal?.orgRole !== role) {
      return ctx.problem(403, "forbidden", `Requires ${role}`);
    }
    return next();
  };
}

// Usage:
middleware: [
  requireClerkAuth(),
  requireOrgRole("org:admin"),
],

Machine-to-machine authentication

Set acceptsToken to "m2m_token", "oauth_token", or an array like ["session_token", "m2m_token"] to accept multiple token kinds. The returned tokenType lets you branch your business logic per caller type.

Webhooks

Clerk delivers user, organization, and session events via Svix-signed webhooks. Use clerk.verifyWebhook(request) to validate the signature before processing the payload — never trust an unverified webhook body.

Runtimes

@clerk/backend is built on the Web Request and fetch APIs, so it runs on Node 18+, Bun, Deno, AWS Lambda, Vercel (Serverless and Edge), and Cloudflare Workers. Pair it with the edge adapters.

See also Auth0, AWS Cognito, and the auth integrations overview.