# Authentication Overview

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

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/authentication/overview*

---

The kit provides production-ready authentication built on [Better Auth](https://better-auth.com), 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](../members-management/permissions-api).

## Features

| Feature | Status | Environment Variable |
|---------|--------|---------------------|
| Email/Password | Enabled by default | `VITE_AUTH_PASSWORD=true` |
| Magic Link | Disabled by default | `VITE_AUTH_MAGIC_LINK=true` |
| Passkey (WebAuthn) | Disabled by default | `ENABLE_PASSKEY` (provision + migrate), then `VITE_AUTH_PASSKEY=true` (UI) |
| Social Providers | Optional | Google is wired by default via `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |
| Multi-Factor Authentication | Optional per user | Enabled in user settings |
| Email Verification | Required by default | Built-in |
| Session Management | Automatic | Cookie-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:

```typescript {% title="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:

```tsx
'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

| Route | Purpose |
|-------|---------|
| `/auth/sign-in` | Sign in with email/password, magic link, or social |
| `/auth/sign-up` | Create new account |
| `/auth/password-reset` | Request password reset email |
| `/auth/verify` | MFA verification (when enabled) |
| `/password-reset` | Set new password from the email link |

## Architecture

The authentication system is split across packages:

| Package | Purpose |
|---------|---------|
| `@kit/better-auth` | Core auth configuration, session context, plugins |
| `@kit/auth` | UI components (sign-in forms, OAuth buttons, MFA) |
| `@kit/function-middleware` | Server function protection (`authFunctionMiddleware`, `organizationFunctionMiddleware`, `adminFunctionMiddleware`) |

### Session Data Structure

```typescript
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](./sign-in)** - Email/password, magic link, and social authentication
2. **[Sign Up](./sign-up)** - User registration and account creation
3. **[Password Reset](./password-reset)** - Self-service password recovery
4. **[Session Handling](./session-handling)** - Protect routes and access session data
5. **[Multi-Factor Authentication](./mfa)** - TOTP-based two-factor authentication

## Environment Variables

Essential auth configuration:

```bash {% title="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 by applying the pre-composed middleware tuples from `@kit/function-middleware` to a literal `createServerFn(...)` call. `authFunctionMiddleware` guarantees `context.user`; continue the standard `createServerFn` chain with `.validator()` and `.handler()`:

```typescript {% title="apps/web/src/lib/.../update-profile.ts" %}
import { createServerFn } from '@tanstack/react-start';
import { z } from 'zod';

import { authFunctionMiddleware } from '@kit/function-middleware/functions';

export const updateProfileFunction = createServerFn({ method: 'POST' })
  .middleware(authFunctionMiddleware)
  .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: updateProfileFunction })`.

### 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:

```typescript {% title="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:

```typescript {% title="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:

```tsx
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.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Which authentication methods are enabled by default?", "answer": "Email/password authentication is enabled by default. Magic link and social providers are disabled by default and can be enabled via environment variables."},
     {"question": "Is email verification required?", "answer": "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."},
     {"question": "How do I add social sign-in?", "answer": "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."},
     {"question": "Where is auth data stored?", "answer": "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."},
     {"question": "How do sessions work?", "answer": "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."},
     {"question": "Can users enable MFA?", "answer": "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](/drizzle).

---

**Next:** [Sign In →](./sign-in)
