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

ParameterTypeDefaultDescription
clientSupabaseClientrequiredSupabase server client
options.verifyMfabooleantrueCheck 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)

LevelMeaning
aal1Basic authentication (password, magic link, OAuth)
aal2MFA verified (TOTP app, etc.)

Error types

ErrorCauseRedirect
AuthenticationErrorUser not logged inSign-in page
MultiFactorAuthErrorMFA required but not verifiedMFA 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 });

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 | null

The 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

  1. User logs in with password/OAuth (reaches aal1)
  2. If MFA is enabled, requireUser checks AAL
  3. If aal1 but MFA required, redirects to MFA verification
  4. After TOTP verification, user reaches aal2
  5. Protected pages now accessible

MFA flow diagram

Login → aal1 → requireUser() → MFA enabled?
Yes: redirect to /auth/verify
User enters TOTP
aal2 → Access granted

Checking 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.ts
import { 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 });
},
},
},
});

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 scope
const client = getSupabaseServerClient();
export async function handler() {
const auth = await requireUser(client); // Won't work
}
// RIGHT: Client created in request context
export async function handler() {
const client = getSupabaseServerClient();
const auth = await requireUser(client);
}

Ignoring the redirectTo property

// WRONG: Not using redirectTo
if (auth.error) {
redirect('/login'); // MFA users sent to wrong page
}
// RIGHT: Use the provided redirectTo
if (auth.error) {
redirect(auth.redirectTo); // Correct handling for auth + MFA
}

Using useUser for server-side checks

// WRONG: useUser is a client-only React hook
export const loadUser = createServerFn({ method: 'GET' }).handler(async () => {
const user = useUser(); // Won't work
});
// RIGHT: Use requireUser on the server
export const loadUser = createServerFn({ method: 'GET' }).handler(async () => {
const client = getSupabaseServerClient();
const auth = await requireUser(client);
});