Authentication Overview

Complete authentication system with email/password, magic links, social providers, MFA, and session management. Built on Better Auth with Drizzle ORM.

The kit provides production-ready authentication built on Better Auth, a TypeScript-first authentication library. All auth state is stored in your Postgres database via Drizzle ORM, giving you full control over user data.

Authentication in MakerKit handles user identity (who you are), while authorization (what you can do) is managed through roles and permissions.

Features

FeatureStatusEnvironment Variable
Email/PasswordEnabled by defaultVITE_AUTH_PASSWORD=true
Magic LinkDisabled by defaultVITE_AUTH_MAGIC_LINK=true
Passkey (WebAuthn)Disabled by defaultENABLE_PASSKEY (provision + migrate), then VITE_AUTH_PASSKEY=true (UI)
Social ProvidersOptionalGoogle is wired by default via GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
Multi-Factor AuthenticationOptional per userEnabled in user settings
Email VerificationRequired by defaultBuilt-in
Session ManagementAutomaticCookie-based

Quick Start

Get the Current Session

Use getSession() from @kit/better-auth/context inside any server context - a server function, a route loader, or a beforeLoad guard. It reads the request headers via getRequestHeaders(), so it only runs on the server. To protect a route, throw a redirect from a beforeLoad guard:

apps/web/src/lib/auth/guards.ts

import { redirect } from '@tanstack/react-router';
import { fetchSession } from './session.functions';
export async function requireAuth({
location,
}: {
location: { href: string };
}) {
const session = await fetchSession();
if (!session) {
throw redirect({
href: `/auth/sign-in?redirect=${encodeURIComponent(location.href)}`,
});
}
return { session };
}

fetchSession is a createServerFn({ method: 'GET' }) wrapper around getSession() (see apps/web/src/lib/auth/session.functions.ts); the wrapper lets beforeLoad/loader read the session server-side even during client-side navigation, where getRequestHeaders() is unavailable. getSession() is memoized per request (via a WeakMap keyed on the request headers), so multiple calls within the same request are efficient.

Client-Side Session

Use authClient.useSession() in any client-rendered component:

'use client';
import { authClient } from '@kit/better-auth/client';
export function UserAvatar() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <Skeleton />;
if (!session) return null;
return <Avatar name={session.user.name} />;
}

Authentication Routes

RoutePurpose
/auth/sign-inSign in with email/password, magic link, or social
/auth/sign-upCreate new account
/auth/password-resetRequest password reset email
/auth/verifyMFA verification (when enabled)
/password-resetSet new password from the email link

Architecture

The authentication system is split across packages:

PackagePurpose
@kit/better-authCore auth configuration, session context, plugins
@kit/authUI components (sign-in forms, OAuth buttons, MFA)
@kit/action-middlewareServer function protection (authAction, organizationAction, adminAction)

Session Data Structure

interface Session {
user: {
id: string;
name: string;
email: string;
image: string | null;
emailVerified: boolean;
createdAt: Date;
updatedAt: Date;
role: string | null; // 'super-admin' for admins
};
session: {
id: string;
userId: string;
expiresAt: Date;
activeOrganizationId: string | null;
};
}

Topics

  1. Sign In - Email/password, magic link, and social authentication
  2. Sign Up - User registration and account creation
  3. Password Reset - Self-service password recovery
  4. Session Handling - Protect routes and access session data
  5. Multi-Factor Authentication - TOTP-based two-factor authentication

Environment Variables

Essential auth configuration:

apps/web/.env

# Required: 32+ character secret for signing tokens
BETTER_AUTH_SECRET=your-secret-key-min-32-characters
# Auth methods (enable/disable)
VITE_AUTH_PASSWORD=true
VITE_AUTH_MAGIC_LINK=false
# Passkey: shows the UI only. Provision it first via ENABLE_PASSKEY in
# packages/better-auth/src/auth.features.ts (regenerate schema + migrate).
VITE_AUTH_PASSKEY=false
# Google OAuth (optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Base URL for auth callbacks
VITE_SITE_URL=http://localhost:3000

Common Patterns

Protect a Server Function

Build protected server functions with the middleware factories from @kit/action-middleware. authAction() pre-binds the authenticated middleware so context.user is guaranteed; continue the standard createServerFn chain with .validator() and .handler():

apps/web/src/lib/.../update-profile.ts

import { z } from 'zod';
import { authAction } from '@kit/action-middleware';
export const updateProfileAction = authAction()
.validator(z.object({ name: z.string().min(1) }))
.handler(async ({ data, context }) => {
// context.user is guaranteed to exist
await updateUser(context.user.id, data);
return { success: true };
});

Call it from a client component with TanStack Query: useMutation({ mutationFn: updateProfileAction }).

Require Organization Context

requireActiveOrganizationId() throws a redirect to the app home path when no organization is active. Call it inside a server function, then load it from a route loader:

organization.functions.ts

import { createServerFn } from '@tanstack/react-start';
import { requireActiveOrganizationId } from '@kit/better-auth/context';
export const fetchMembers = createServerFn({ method: 'GET' }).handler(
async () => {
// Redirects to the app home path if not in org context
const orgId = await requireActiveOrganizationId();
return loadMembers(orgId);
},
);

Check Admin Status

Use isUserAdmin() from @kit/auth/require-admin inside a server function to check admin status without redirecting, then expose it to the UI via a loader:

apps/web/src/lib/auth/session.functions.ts

import { createServerFn } from '@tanstack/react-start';
import { isUserAdmin } from '@kit/auth/require-admin';
export const fetchIsAdmin = createServerFn({ method: 'GET' }).handler(() =>
isUserAdmin(),
);

In a component, render the admin link with Link from @tanstack/react-router based on the loader data:

import { Link } from '@tanstack/react-router';
<nav>
<Link to="/dashboard">Dashboard</Link>
{isAdmin && <Link to="/admin">Admin</Link>}
</nav>;

Common Pitfalls

These issues come up frequently in production deployments:

  1. Missing BETTER_AUTH_SECRET: The secret must be at least 32 characters. A short or missing secret causes cryptic token errors.
  2. Callback URL mismatch: Social providers require exact callback URLs. Make sure VITE_SITE_URL matches your deployment URL, including https:// in production.
  3. Cookie issues across subdomains: If deploying to multiple subdomains, you may need to configure the cookie domain in Better Auth settings.
  4. Session not found after deploy: Clear browser cookies after changing BETTER_AUTH_SECRET, as old sessions become invalid.
  5. MFA bypassed via magic link: By design, magic link and social auth skip MFA. If you require MFA for all users, disable these methods.

Frequently Asked Questions

Which authentication methods are enabled by default?
Email/password authentication is enabled by default. Magic link and social providers are disabled by default and can be enabled via environment variables.
Is email verification required?
Yes, email verification is required by default. Users must verify their email before they can fully access the application. This is configured in the Better Auth settings.
How do I add social sign-in?
Google is wired into the current repo. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, then include google in VITE_AUTH_OAUTH_PROVIDERS. For GitHub or another provider, extend packages/better-auth/src/plugins/social-providers.ts first.
Where is auth data stored?
All authentication data (users, sessions, accounts) is stored in your Postgres database via Drizzle ORM. Better Auth manages the schema and provides type-safe queries.
How do sessions work?
Sessions are stored in the database and referenced via HTTP-only cookies. The getSession() function retrieves the current session and is memoized per request for efficiency.
Can users enable MFA?
Yes, users can enable TOTP-based MFA from their security settings at /settings/security. Once enabled, they must enter a code from their authenticator app after signing in with email/password.

This authentication system is part of the TanStack Start Drizzle SaaS Kit.


Next: Sign In →