next-shell

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 */
  },
};

On this page