Authentication API
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 functions, route loaders, and server routes. It handles both standard auth and MFA verification in a single call.
import { redirect } from '@tanstack/react-router';import { createServerFn } from '@tanstack/react-start';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export const loadCurrentUser = createServerFn({ method: 'GET' }).handler( async () => { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { throw redirect({ href: auth.redirectTo }); } return auth.data; },);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 Route Loaders
Call the server function from a route loader, then read the result in the component with Route.useLoaderData():
import { createFileRoute, redirect } from '@tanstack/react-router';import { createServerFn } from '@tanstack/react-start';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';const loadDashboard = createServerFn({ method: 'GET' }).handler(async () => { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { throw redirect({ href: auth.redirectTo }); } return auth.data;});export const Route = createFileRoute('/dashboard')({ loader: () => loadDashboard(), component: DashboardPage,});function DashboardPage() { const user = Route.useLoaderData(); return ( <div> <h1>Dashboard</h1> <p>Logged in as: {user.email}</p> <p>MFA status: {user.aal === 'aal2' ? 'Verified' : 'Not verified'}</p> </div> );}Usage in Server Functions
import { redirect } from '@tanstack/react-router';import { createServerFn } from '@tanstack/react-start';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export const updateProfileFunction = createServerFn({ method: 'POST' }) .validator(UpdateProfileSchema) .handler(async ({ data }) => { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { throw redirect({ href: auth.redirectTo }); } await client .from('profiles') .update({ name: data.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 });MFA security
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.
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
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.
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 Server Route
// apps/web/src/routes/api/user.tsimport { createFileRoute } from '@tanstack/react-router';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';export const Route = createFileRoute('/api/user')({ server: { handlers: { GET: async () => { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const { data: profile } = await client .from('profiles') .select('*') .eq('id', auth.data.id) .single(); return Response.json({ user: auth.data, profile }); }, }, },});Using authFunctionMiddleware (recommended)
The authFunctionMiddleware tuple handles authentication automatically and injects the current user into context:
import { createServerFn } from '@tanstack/react-start';import * as z from 'zod';import { authFunctionMiddleware } from '@kit/function-middleware/functions';import { getSupabaseServerClient } from '@kit/supabase/server-client';const UpdateProfileSchema = z.object({ name: z.string().min(2),});export const updateProfileFunction = createServerFn({ method: 'POST' }) .middleware(authFunctionMiddleware) .validator(UpdateProfileSchema) .handler(async ({ data, context: { 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 }; });Public server functions (no auth)
Omit the auth middleware for endpoints that don't require a signed-in user:
import { createServerFn } from '@tanstack/react-start';export const submitContactFunction = createServerFn({ method: 'POST' }) .validator(ContactFormSchema) .handler(async ({ data }) => { // No user context in public server functions await sendEmail(data); return { success: true }; });Role-based access control
Combine authentication with role checks:
import { createFileRoute, redirect } from '@tanstack/react-router';import { createServerFn } from '@tanstack/react-start';import { isSuperAdmin } from '@kit/admin';import { requireUser } from '@kit/supabase/require-user';import { getSupabaseServerClient } from '@kit/supabase/server-client';const requireAdmin = createServerFn({ method: 'GET' }).handler(async () => { const client = getSupabaseServerClient(); const auth = await requireUser(client); if (auth.error) { throw redirect({ href: auth.redirectTo }); } const isAdmin = await isSuperAdmin(client); if (!isAdmin) { throw redirect({ to: '/dashboard' }); } return auth.data;});export const Route = createFileRoute('/admin')({ loader: () => requireAdmin(), component: AdminDashboard,});For super-admin server functions, prefer the adminFunctionMiddleware tuple from @kit/function-middleware/functions, and use withMinRole / withFeaturePermission from @kit/function-middleware/server to gate team-scoped operations.
Auth state listener (Client)
For real-time auth state changes:
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 a client-only React hookexport const loadUser = createServerFn({ method: 'GET' }).handler(async () => { const user = useUser(); // Won't work});// RIGHT: Use requireUser on the serverexport const loadUser = createServerFn({ method: 'GET' }).handler(async () => { const client = getSupabaseServerClient(); const auth = await requireUser(client);});Related documentation
- Account API - Personal account operations
- Writing Data to the Database - Using server functions and auth middleware
- Function Middleware - Auth, admin, and permission gates for server functions