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
| Feature | Status | Environment Variable |
|---|---|---|
| Email/Password | Enabled by default | NEXT_PUBLIC_AUTH_PASSWORD=true |
| Magic Link | Disabled by default | NEXT_PUBLIC_AUTH_MAGIC_LINK=true |
| Social Providers (Google, GitHub) | Optional | 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() in any server context:
import { getSession } from '@kit/better-auth/context';export default async function DashboardPage() { const session = await getSession(); if (!session) { redirect('/auth/sign-in'); } return <div>Welcome, {session.user.name}</div>;}The function is cached per request via React's cache(), so multiple calls within the same request are efficient.
Client-Side Session
Use authClient.useSession() in client components:
'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) |
/reset-password | Set new password (from 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/action-middleware | Server action protection |
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
- Sign In - Email/password, magic link, and social authentication
- Sign Up - User registration and account creation
- Password Reset - Self-service password recovery
- Session Handling - Protect routes and access session data
- Multi-Factor Authentication - TOTP-based two-factor authentication
Environment Variables
Essential auth configuration:
apps/web/.env.local
# Required: 32+ character secret for signing tokensBETTER_AUTH_SECRET=your-secret-key-min-32-characters# Auth methods (enable/disable)NEXT_PUBLIC_AUTH_PASSWORD=trueNEXT_PUBLIC_AUTH_MAGIC_LINK=false# Google OAuth (optional)GOOGLE_CLIENT_ID=your-google-client-idGOOGLE_CLIENT_SECRET=your-google-client-secret# Base URL for auth callbacksNEXT_PUBLIC_SITE_URL=http://localhost:3000Common Patterns
Protect a Server Action
'use server';import { authenticatedActionClient } from '@kit/action-middleware';import { z } from 'zod';export const updateProfileAction = authenticatedActionClient .inputSchema(z.object({ name: z.string().min(1) })) .action(async ({ parsedInput, ctx }) => { // ctx.user is guaranteed to exist await updateUser(ctx.user.id, parsedInput); return { success: true }; });Require Organization Context
import { requireActiveOrganizationId } from '@kit/better-auth/context';export default async function TeamPage() { // Redirects to /dashboard if not in org context const orgId = await requireActiveOrganizationId(); const members = await loadMembers(orgId); return <MembersList members={members} />;}Check Admin Status
import { isUserAdmin } from '@kit/auth/require-admin';export default async function Header() { const isAdmin = await isUserAdmin(); return ( <nav> <Link href="/dashboard">Dashboard</Link> {isAdmin && <Link href="/admin">Admin</Link>} </nav> );}Common Pitfalls
These issues come up frequently in production deployments:
- Missing
BETTER_AUTH_SECRET: The secret must be at least 32 characters. A short or missing secret causes cryptic token errors. - Callback URL mismatch: Social providers require exact callback URLs. Make sure
NEXT_PUBLIC_SITE_URLmatches your deployment URL, includinghttps://in production. - Cookie issues across subdomains: If deploying to multiple subdomains, you may need to configure the cookie domain in Better Auth settings.
- Session not found after deploy: Clear browser cookies after changing
BETTER_AUTH_SECRET, as old sessions become invalid. - 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?
Is email verification required?
How do I add Google or GitHub sign-in?
Where is auth data stored?
How do sessions work?
Can users enable MFA?
This authentication system is part of the Next.js Drizzle SaaS Kit.
Next: Sign In →