Auth
Pluggable auth adapter pattern — wire in Auth.js, Clerk, Supabase, or a custom backend.
The auth system is adapter-based: AuthProvider accepts any object that implements the AuthAdapter interface, so you can swap backends without touching component code.
Quick start with Auth.js v5
pnpm add next-auth@beta
import { createNextAuthAdapter } from '@jonmatum/next-shell/auth/nextauth';
import { auth } from '@/auth'; // your Auth.js handler
import { AuthProvider } from '@jonmatum/next-shell/auth';
export default function RootLayout({ children }) {
return (
<AuthProvider adapter={createNextAuthAdapter({ getServerSession: auth })}>
{children}
</AuthProvider>
);
}
Hooks
useSession
Returns the raw session result:
import { useSession } from '@jonmatum/next-shell/auth';
function Status() {
const { data, status } = useSession();
if (status === 'loading') return <Spinner />;
if (status === 'unauthenticated') return <a href="/login">Sign in</a>;
return <span>Hello, {data.user.name}</span>;
}
useUser
Shorthand for useSession().data?.user ?? null:
const user = useUser();
useHasPermission
AND-semantics permission check against user.roles and user.scopes. Mirrors the nav requires field:
const isAdmin = useHasPermission('admin');
const canEdit = useHasPermission(['editor', 'posts:write']);
useRequireAuth
Suspense-compatible — throws while loading, throws AuthRequiredError when unauthenticated:
function SecretPage() {
const user = useRequireAuth();
return <div>Welcome, {user.name}</div>;
}
// Wrap in Suspense + ErrorBoundary:
<ErrorBoundary fallback={<Redirect to="/login" />}>
<Suspense fallback={<Spinner />}>
<SecretPage />
</Suspense>
</ErrorBoundary>;
Conditional rendering
import { SignedIn, SignedOut, RoleGate } from '@jonmatum/next-shell/auth';
function Nav() {
return (
<>
<SignedIn fallback={<Skeleton />}>
<UserMenu />
</SignedIn>
<SignedOut>
<a href="/login">Sign in</a>
</SignedOut>
<RoleGate role="admin" fallback={null}>
<AdminLink />
</RoleGate>
</>
);
}
Server-side protection
import { requireSession } from '@jonmatum/next-shell/auth/server';
import { auth } from '@/auth';
export async function GET() {
// Throws AuthRequiredError (statusCode: 401) when unauthenticated
const session = await requireSession(auth);
return Response.json({ user: session.user });
}
Testing with the mock adapter
import { createMockAuthAdapter } from '@jonmatum/next-shell/auth/mock';
import { AuthProvider } from '@jonmatum/next-shell/auth';
render(
<AuthProvider
adapter={createMockAuthAdapter({
user: { id: '1', name: 'Alice', roles: ['admin'] },
})}
>
<MyComponent />
</AuthProvider>,
);
The onSignIn and onSignOut options let you spy on auth calls in tests.
Custom adapters
Implement AuthAdapter to support any backend (Clerk, Supabase, custom JWT):
import type { AuthAdapter } from '@jonmatum/next-shell/auth';
import { useAuth } from '@clerk/nextjs';
const clerkAdapter: AuthAdapter = {
useSession() {
const { isLoaded, isSignedIn, userId } = useAuth();
if (!isLoaded) return { data: null, status: 'loading' };
if (!isSignedIn) return { data: null, status: 'unauthenticated' };
return {
data: { user: { id: userId! }, expires: '' },
status: 'authenticated',
};
},
async signIn() {
/* redirect to Clerk sign-in */
},
async signOut() {
/* Clerk sign-out */
},
};