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
- 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.
- Your frontend (Clerk's React, Next.js, Expo, or vanilla JS SDK) obtains a session token via
getToken()and sends it in theAuthorization: Bearer <token>header to your DaloyJS API. - For machine-to-machine calls, create an M2M tokenor use Clerk's OAuth applications and accept
oauth_token/m2m_tokenin the verifier.
2. Install
pnpm add @clerk/backend3. Environment variables
# .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
// 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
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:
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.