Clerk Authentication
Integrate Clerk into your ejected Dinou application to handle user authentication, route protection, and database synchronization.
Since Dinou is fully ejectable, you have complete control over the Express server. You can integrate Clerk's official Express middleware directly, propagate session context, and sync events via webhooks.
💡 Core Concept
Clerk integration in Dinou is structured around three main components:
__session cookie manually ensures the user ID is reliably extracted even in dynamic compiling child processes.getContext().req.userId to secure actions.svix and synchronize Clerk user profile events (like creation and deletion) to your local database.📦 1. Installation & Env
First, install Clerk's Express SDK, React SDK, localization assets, and the Svix package (necessary to verify signature headers of incoming Clerk webhooks):
npm install @clerk/express @clerk/react @clerk/localizations svixAdd your Clerk credentials to your local .env file:
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
CLERK_WEBHOOK_SECRET=whsec_...🌐 2. Express Server Setup
Once ejected, open your local dinou/core/server.js file.
Importing and Initializing Clerk
Import the Clerk middleware and initializers using CommonJS at the top of the server file:
const { clerkMiddleware, getAuth } = require("@clerk/express");Adding Verified User ID Helper
To extract the authenticated user ID safely, we define a helper that retrieves the User ID exclusively from Clerk's cryptographically verified request objects.
app.use(clerkMiddleware());
// 🛡️ Helper: Extracts userId only if verified cryptographically by Clerk
function getUserIdFallback(req) {
// Returns the ID verified by Clerk's middleware
return req.auth?.userId || getAuth(req)?.userId || null;
}🔒 Security Notice: Cryptographic Verification
__session cookie using raw Base64 decoders for authorization decisions. An attacker can easily forge a cookie with any User ID. Always rely on Clerk's official verified bindings (req.auth or getAuth), which cryptographically validate the token signature using your CLERK_SECRET_KEY.Context Propagation
Expose the active userId inside the request context. In server.js, update getContext and getContextForServerFunctionEndpoint:
function getContext(req, res) {
return {
req: {
cookies: { ...req.cookies },
headers: { ... },
query: { ...req.query },
path: req.path,
method: req.method,
userId: getUserIdFallback(req), // 👈 Expose Clerk user ID
},
res: { ... }
};
}
function getContextForServerFunctionEndpoint(req, res) {
return {
req: {
cookies: { ...req.cookies },
headers: { ... },
query: { ...req.query },
path: req.path,
method: req.method,
userId: getUserIdFallback(req), // 👈 Expose Clerk user ID
},
res: { ... }
};
}Also, propagate this ID in the Express wildcard GET handler (app.get(/^\/.*\/?$/)) inside contextForChild for Server Component rendering:
const contextForChild = {
req: {
query: { ...req.query },
cookies: { ...req.cookies },
headers: { ... },
path: req.path,
method: req.method,
userId: getUserIdFallback(req), // 👈 Propagate userId here
},
};🔑 3. Usage in RSC & Server Functions
Now, you can check the session from any Server Component, Page Function, or Server Function:
Securing Server Functions
Server Functions run mutations on the server. You can protect them by fetching userId directly from the context:
// src/actions.ts (Server Function)
"use server";
import { getContext } from "dinou";
export async function createPost(title: string, content: string) {
const context = getContext();
const userId = context?.req?.userId;
// 🚨 Security Check
if (!userId) {
throw new Error("Unauthorized access. Invalid session.");
}
// Session is valid, save record to database
const post = await prisma.post.create({
data: {
title,
content,
authorClerkId: userId
}
});
return post;
}Fetching User Data in Page Functions
Load user-specific data from your database inside page_functions.ts before compiling the page:
// src/dashboard/page_functions.ts
import { getContext } from "dinou";
export function dynamic() {
return true; // Force SSR at request-time to load dynamic session
}
export async function getProps() {
const context = getContext();
const userId = context?.req?.userId;
if (!userId) {
return {
redirect: {
destination: "/login",
}
};
}
const posts = await prisma.post.findMany({
where: { authorClerkId: userId }
});
return {
page: {
posts,
userId
}
};
}⚛️ 4. Client Setup (DinouClerkProvider)
To enable smooth Client-side SPA navigation and dynamic language changes in Clerk's built-in login forms, create a custom wrapper around ClerkProvider:
// src/components/DinouClerkProvider.tsx
"use client";
import { ReactNode } from "react";
import { ClerkProvider } from "@clerk/react";
import { useRouter } from "dinou";
import { esES, enUS } from "@clerk/localizations";
// Customize localization text to match your app's brand
const customEnUS = {
...enUS,
formFieldLabel__emailAddress: "Email address (only sign in)",
signIn: {
...enUS.signIn,
start: {
...enUS.signIn.start,
title: "Sign in / up",
subtitle: "to continue to your app",
},
},
};
const customEsES = {
...esES,
formFieldLabel__emailAddress: "Correo electrónico (solo iniciar sesión)",
signIn: {
...esES.signIn,
start: {
...esES.signIn.start,
title: "Iniciar sesión / Registro",
subtitle: "para continuar a tu app",
},
},
};
const clerkLocales = {
es: customEsES,
en: customEnUS,
};
export function DinouClerkProvider({
children,
publishableKey,
locale = "en",
}: {
children: ReactNode;
publishableKey: string;
locale?: "en" | "es";
}) {
const router = useRouter();
return (
<ClerkProvider
publishableKey={publishableKey}
// Integrate with Dinou client router for smooth SPA soft transitions
navigate={(to) => router.push(to)}
// Dynamically select custom translations mapping the active locale
localization={clerkLocales[locale]}
appearance={{
elements: {
footerAction: { display: "none" }, // Optional: Hide sign up link
},
}}
>
{children}
</ClerkProvider>
);
}Wrapping the Root Layout
In your root layout.tsx, retrieve the locale from context and wrap the tree with your custom provider:
// src/layout.tsx
import { getContext } from "dinou";
import { DinouClerkProvider } from "@/components/DinouClerkProvider";
export default function Layout({ children }) {
const context = getContext();
const locale = context?.req?.locale || "en";
return (
<html lang={locale}>
<body>
<DinouClerkProvider
publishableKey={process.env.CLERK_PUBLISHABLE_KEY || ""}
locale={locale}
>
{children}
</DinouClerkProvider>
</body>
</html>
);
}🔄 5. Handling Clerk Webhooks
To synchronize Clerk users with your local database (e.g. Prisma), create a POST webhook endpoint in dinou/core/server.js. Use the svix SDK to verify the authenticity of Clerk's payload signature:
import { Webhook } from "svix";
// Endpoint to receive webhook events from Clerk (e.g. user.created)
app.post("/api/webhooks/clerk", express.raw({ type: "application/json" }), async (req, res) => {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) {
console.error("Missing CLERK_WEBHOOK_SECRET in .env");
return res.status(500).send("Server Error");
}
const svix_id = req.headers["svix-id"];
const svix_timestamp = req.headers["svix-timestamp"];
const svix_signature = req.headers["svix-signature"];
if (!svix_id || !svix_timestamp || !svix_signature) {
return res.status(400).send("Missing Svix headers");
}
// Clerk webhook verification expects raw string payload
const payloadString = req.body.toString("utf8");
const wh = new Webhook(WEBHOOK_SECRET);
let evt;
try {
evt = wh.verify(payloadString, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
console.error("Clerk Webhook verification failed:", err.message);
return res.status(400).json({ error: "Invalid signature" });
}
const { id } = evt.data;
const eventType = evt.type;
if (eventType === "user.created") {
const email = evt.data.email_addresses[0]?.email_address || `${id}@no-email.com`;
try {
// Sync user to local Prisma database
await prisma.user.upsert({
where: { clerkId: id },
update: { email },
create: {
clerkId: id,
email,
},
});
} catch (dbErr) {
console.error("Database user sync failed:", dbErr.message);
return res.status(500).send("Database Error");
}
}
if (eventType === "user.deleted") {
try {
// Remove user from local database
await prisma.user.delete({
where: { clerkId: id },
});
} catch (dbErr) {
// Ignore error if user does not exist
}
}
res.status(200).json({ received: true });
});⚠️ Raw Request Body Notice
app.use(express.json())) or uses local middleware like express.raw(). This keeps the raw request buffer untouched for cryptographic Svix signature verification.