Bridge Training
Auth

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

HookReturns
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

FunctionReturnsCost
getSessionUserId()Clerk user id or nullLocal JWT verification, no Clerk API call
getSessionToken()The session bearer or nullLocal
getSessionIdentitySnapshot()IdentitySnapshot or nullOne 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');