Authentication API | Next.js Supabase SaaS Kit
Complete reference for authentication in MakerKit. Use requireUser for server-side auth checks, handle MFA verification, and access user data in client components.
The Authentication API verifies user identity, handles MFA (Multi-Factor Authentication), and provides user data to your components. Use requireUser on the server for protected routes and useUser on the client for reactive user state.
Authentication API Reference
Learn how to authenticate users in MakerKit
requireUser (Server)
The requireUser function checks authentication status in Server Components, Server Actions, and Route Handlers. It handles both standard auth and MFA verification in a single call.
import { redirect } from 'next/navigation';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function ProtectedPage() { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { redirect(auth.redirectTo); } const user = auth.data; return <div>Welcome, {user.email}</div>;}Function signature
function requireUser( client: SupabaseClient, options?: { verifyMfa?: boolean; // Default: true }): Promise<RequireUserResponse>Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
client | SupabaseClient | required | Supabase server client |
options.verifyMfa | boolean | true | Check MFA status |
Response types
Success response:
{ data: { id: string; // User UUID email: string; // User email phone: string; // User phone (if set) is_anonymous: boolean; // Anonymous auth flag aal: 'aal1' | 'aal2'; // Auth Assurance Level app_metadata: Record<string, unknown>; user_metadata: Record<string, unknown>; amr: AMREntry[]; // Auth Methods Reference }; error: null;}Error response:
{ data: null; error: AuthenticationError | MultiFactorAuthError; redirectTo: string; // Where to redirect the user}Auth Assurance Levels (AAL)
| Level | Meaning |
|---|---|
aal1 | Basic authentication (password, magic link, OAuth) |
aal2 | MFA verified (TOTP app, etc.) |
Error types
| Error | Cause | Redirect |
|---|---|---|
AuthenticationError | User not logged in | Sign-in page |
MultiFactorAuthError | MFA required but not verified | MFA verification page |
Usage in Server Components
import { redirect } from 'next/navigation';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export default async function DashboardPage() { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { redirect(auth.redirectTo); } return ( <div> <h1>Dashboard</h1> <p>Logged in as: {auth.data.email}</p> <p>MFA status: {auth.data.aal === 'aal2' ? 'Verified' : 'Not verified'}</p> </div> );}Usage in Server Actions
'use server';import { redirect } from 'next/navigation';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function updateProfile(formData: FormData) { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { redirect(auth.redirectTo); } const name = formData.get('name') as string; await client .from('profiles') .update({ name }) .eq('id', auth.data.id); return { success: true };}Skipping MFA verification
For pages that don't require full MFA verification:
const auth = await requireUser(client, { verifyMfa: false });Only disable MFA verification for non-sensitive pages. Always verify MFA for billing, account deletion, and other high-risk operations.
useUser (Client)
The useUser hook provides reactive access to user data in client components. It reads from the auth context and updates automatically on auth state changes.
'use client';import { useUser } from '@kit/supabase/hooks/use-user';function UserMenu() { const user = useUser(); if (!user) { return <div>Loading...</div>; } return ( <div> <span>{user.email}</span> <img src={user.user_metadata.avatar_url} alt="Avatar" /> </div> );}Return type
User | nullThe User type from Supabase includes:
{ id: string; email: string; phone: string; created_at: string; updated_at: string; app_metadata: { provider: string; providers: string[]; }; user_metadata: { avatar_url?: string; full_name?: string; // Custom metadata fields }; aal?: 'aal1' | 'aal2';}Conditional rendering
'use client';import { useUser } from '@kit/supabase/hooks/use-user';function ConditionalContent() { const user = useUser(); // Show loading state if (user === undefined) { return <Skeleton />; } // Not authenticated if (!user) { return <LoginPrompt />; } // Authenticated return <UserDashboard user={user} />;}useSupabase (Client)
The useSupabase hook provides the Supabase browser client for client-side operations.
'use client';import { useSupabase } from '@kit/supabase/hooks/use-supabase';import { useQuery } from '@tanstack/react-query';function TaskList() { const supabase = useSupabase(); const { data: tasks } = useQuery({ queryKey: ['tasks'], queryFn: async () => { const { data, error } = await supabase .from('tasks') .select('*') .order('created_at', { ascending: false }); if (error) throw error; return data; }, }); return ( <ul> {tasks?.map((task) => ( <li key={task.id}>{task.title}</li> ))} </ul> );}MFA handling
MakerKit automatically handles MFA verification through the requireUser function.
How it works
- User logs in with password/OAuth (reaches
aal1) - If MFA is enabled,
requireUserchecks AAL - If
aal1but MFA required, redirects to MFA verification - After TOTP verification, user reaches
aal2 - Protected pages now accessible
MFA flow diagram
Login → aal1 → requireUser() → MFA enabled? ↓ Yes: redirect to /auth/verify ↓ User enters TOTP ↓ aal2 → Access grantedChecking MFA status
import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';async function checkMfaStatus() { const client = getSupabaseServerClient(); const auth = await requireUser(client, { verifyMfa: false }); if (auth.error) { return { authenticated: false }; } return { authenticated: true, mfaEnabled: auth.data.aal === 'aal2', authMethods: auth.data.amr.map((m) => m.method), };}Common patterns
Protected API Route Handler
// app/api/user/route.tsimport { NextResponse } from 'next/server';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function GET() { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const { data: profile } = await client .from('profiles') .select('*') .eq('id', auth.data.id) .single(); return NextResponse.json({ user: auth.data, profile });}Using enhanceAction (recommended)
The enhanceAction utility handles authentication automatically:
'use server';import { z } from 'zod';import { enhanceAction } from '@kit/next/actions';import { getSupabaseServerClient } from '@kit/supabase/server-client';const UpdateProfileSchema = z.object({ name: z.string().min(2),});export const updateProfile = enhanceAction( async (data, user) => { // user is automatically available and typed const client = getSupabaseServerClient(); await client .from('profiles') .update({ name: data.name }) .eq('id', user.id); return { success: true }; }, { schema: UpdateProfileSchema, auth: true, // Default: requires authentication });Public actions (no auth)
export const submitContactForm = enhanceAction( async (data) => { // No user parameter when auth: false await sendEmail(data); return { success: true }; }, { schema: ContactFormSchema, auth: false, });Role-based access control
Combine authentication with role checks:
import { redirect } from 'next/navigation';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';import { isSuperAdmin } from '@kit/admin';async function AdminPage() { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { redirect(auth.redirectTo); } const isAdmin = await isSuperAdmin(client); if (!isAdmin) { redirect('/home'); } return <AdminDashboard />;}Auth state listener (Client)
For real-time auth state changes:
'use client';import { useEffect } from 'react';import { useSupabase } from '@kit/supabase/hooks/use-supabase';function AuthStateListener({ onAuthChange }) { const supabase = useSupabase(); useEffect(() => { const { data: { subscription }, } = supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_IN') { onAuthChange({ type: 'signed_in', user: session?.user }); } else if (event === 'SIGNED_OUT') { onAuthChange({ type: 'signed_out' }); } else if (event === 'TOKEN_REFRESHED') { onAuthChange({ type: 'token_refreshed' }); } }); return () => subscription.unsubscribe(); }, [supabase, onAuthChange]); return null;}Common mistakes
Creating client at module scope
// WRONG: Client created at module scopeconst client = getSupabaseServerClient();export async function handler() { const auth = await requireUser(client); // Won't work}// RIGHT: Client created in request contextexport async function handler() { const client = getSupabaseServerClient(); const auth = await requireUser(client);}Ignoring the redirectTo property
// WRONG: Not using redirectToif (auth.error) { redirect('/login'); // MFA users sent to wrong page}// RIGHT: Use the provided redirectToif (auth.error) { redirect(auth.redirectTo); // Correct handling for auth + MFA}Using useUser for server-side checks
// WRONG: useUser is client-onlyexport async function ServerComponent() { const user = useUser(); // Won't work}// RIGHT: Use requireUser on serverexport async function ServerComponent() { const client = getSupabaseServerClient(); const auth = await requireUser(client);}Related documentation
- Account API - Personal account operations
- Server Actions - Using enhanceAction
- Route Handlers - API authentication