Next.js
Client and server helpers for a Next.js app, the only place @clerk/nextjs is imported.
Two entries cover the admin console. @workspace/auth/nextjs is the client side, @workspace/auth/server/nextjs the server side. Neither uses the store-backed hooks: Next.js apps read the session through Clerk's own context, wrapped here so the vendor stays inside the package.
Client: @workspace/auth/nextjs
AuthProvider
Clerk's provider, keys read from NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY as Clerk's Next.js SDK does. Wrap the root layout with it.
import { AuthProvider } from '@workspace/auth/nextjs';
export default function RootLayout({ children }) {
return (
<AuthProvider>
<html lang="en">
<body>{children}</body>
</html>
</AuthProvider>
);
}SignIn
Clerk's sign-in component with its full prop surface. The admin mounts it on an optional catch-all route so Clerk can handle its own sub-steps:
// app/(auth)/sign-in/[[...rest]]/page.tsx
import { SignIn } from '@workspace/auth/nextjs';
<SignIn forceRedirectUrl="/" path="/sign-in" routing="path" />Hooks
| Hook | Returns |
|---|---|
useSessionUser() | SessionUser or null while loading or signed out |
useSignOut() | (options: { redirectUrl }) => Promise<void> |
useOpenUserProfile() | () => void, opens Clerk's profile modal |
Prop
Type
'use client';
import { useSessionUser, useSignOut } from '@workspace/auth/nextjs';
const user = useSessionUser();
const signOut = useSignOut();
<button onClick={() => signOut({ redirectUrl: '/sign-in' })}>{user?.email}</button>Server: @workspace/auth/server/nextjs
Import only from server components, server actions and proxy.ts. These helpers read the request's Clerk session.
createAuthProxy
A Next.js middleware that requires a session on every route outside publicRoutes. Without signInUrl, Clerk redirects signed-out users to its hosted account portal instead of the app's own page.
// src/proxy.ts
import { createAuthProxy } from '@workspace/auth/server/nextjs';
export default createAuthProxy({
publicRoutes: ['/sign-in(.*)', '/sign-out', '/unauthorized', '/api(.*)'],
signInUrl: '/sign-in',
});
export const config = {
matcher: ['/((?!_next|.*\\..*).*)', '/(api|trpc)(.*)'],
};Prop
Type
API routes are listed as public here and guard themselves, answering JSON 401 or 403 rather than redirecting.
Session reads
| Function | Returns | Cost |
|---|---|---|
getSessionUserId() | Clerk user id or null | Local JWT verification, no Clerk API call |
getSessionToken() | The session bearer or null | Local |
getSessionIdentitySnapshot() | IdentitySnapshot or null | One Clerk API call |
Use the id for every lookup, and the snapshot only to create an account on first contact:
import 'server-only';
import { getSessionIdentitySnapshot, getSessionUserId } from '@workspace/auth/server/nextjs';
export const getCurrentAccount = cache(async () => {
const userId = await getSessionUserId();
if (!userId) return null;
const existing = await accountsService.getByIdentityProviderUserId(userId);
if (existing) return existing;
const snapshot = await getSessionIdentitySnapshot();
if (!snapshot) return null;
if (!snapshot.signupSource) redirect('/no-account');
return accountsService.upsertFromIdentitySnapshot({ ...snapshot, signupSource: snapshot.signupSource });
});The snapshot carries the role found in Clerk public metadata and the signup source, read by the one rule of every creation path. Both are applied on creation only: afterwards the accounts row is the source of truth, never a Clerk claim. Without a signup source no account is created.
getSessionToken() is what a server action forwards to the API when it calls it on the admin's behalf, for example to start an impersonation.
inviteUser
Creates a Clerk invitation that carries the role and the space (signupSource) in public metadata, so the invited user's account is created with them on first sign-in.
import { inviteUser } from '@workspace/auth/server/nextjs';
await inviteUser({ emailAddress, role: 'admin', signupSource: 'app', redirectUrl: `${env.ADMIN_BASE_URL}/sign-in` });Prop
Type
syncUserRole
Mirrors a role change into Clerk public metadata, after the database row was updated. The metadata is informational: nothing reads it back except the first-contact seed above.
import { syncUserRole } from '@workspace/auth/server/nextjs';
await syncUserRole(account.identityProviderUserId, 'teacher');