Skip to main content
The Auth UI Boilerplate follows Next.js App Router conventions with a clean separation of concerns.

Directory Overview

src/
├── app/                     # Next.js App Router pages and API routes
│   ├── api/auth/[...all]/   # Better Auth handler
│   ├── api/[...path]/       # JWT-injecting API proxy
│   ├── login/ & signup/     # Authentication pages
│   └── page.tsx             # Home page
├── components/              # React components
│   ├── ui/                  # shadcn/ui primitives
│   └── ...                  # Auth status, API test components
├── lib/                     # Utilities and configuration
│   ├── auth.ts              # Better Auth server config
│   ├── auth-client.ts       # Better Auth client config
│   ├── api-client.ts        # Fetch-based API client
│   └── api-client-axios.ts  # Axios-based API client
└── db/                      # Database layer
    ├── schema.ts            # Drizzle ORM schema
    ├── migrations/          # SQL migration files
    └── index.ts             # Database client

App Directory (src/app/)

The app/ directory uses Next.js 13+ App Router with file-based routing.

Authentication Routes

// Handles all Better Auth endpoints
// Routes: /api/auth/sign-in, /api/auth/sign-up, /api/auth/callback/*, etc.
import { auth } from "@/lib/auth"
import { toNextJsHandler } from "better-auth/next-js"

export const { GET, POST } = toNextJsHandler(auth)

API Proxy (src/app/api/[...path]/route.ts)

The catch-all API proxy automatically injects JWT tokens into requests forwarded to your backend.
Any request to /api/* (except /api/auth/*) is automatically proxied to your backend with JWT authentication.
How it works:
  1. Client makes request to /api/users/me
  2. Proxy fetches JWT from Better Auth using the current session
  3. JWT is injected as Authorization: Bearer <token> header
  4. Request is forwarded to BACKEND_API_URL/api/users/me
  5. Backend response is returned to client
Configuration: Set BACKEND_API_URL in your .env file (server-side only):
.env
BACKEND_API_URL=http://localhost:8080

Components Directory (src/components/)

UI Components (src/components/ui/)

Pre-built shadcn/ui components:
  • button.tsx - Button primitive
  • card.tsx - Card container
  • input.tsx - Form input

Demo Components

The boilerplate includes demo components that you can remove when building your app:
  • auth-status.tsx - Displays current authentication status
  • api-test.tsx - Tests the fetch-based API client
  • api-test-axios.tsx - Tests the Axios-based API client

Core Components

Keep these components in your production app:
  • login-required.tsx - HOC for protecting authenticated routes
  • theme-provider.tsx - Dark/light mode provider
  • theme-toggle.tsx - Theme switcher button
  • fade-in.tsx - Animation wrapper

Library Directory (src/lib/)

Authentication Configuration

// Better Auth server configuration
// Defines providers, session handling, JWT settings
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "pg" }),
  emailAndPassword: { enabled: true },
  socialProviders: {
    google: {
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    },
  },
})

API Clients

Two API client implementations are provided:
  • api-client.ts - Fetch-based (zero dependencies)
  • api-client-axios.ts - Axios-based (with interceptors)
See API Clients for detailed usage.

Database Directory (src/db/)

Schema Definition (src/db/schema.ts)

The database schema is defined using Drizzle ORM. Better Auth automatically manages these tables:
src/db/schema.ts
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"

// Auto-generated by Better Auth
export const user = pgTable("user", {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  emailVerified: boolean('email_verified').$defaultFn(() => false).notNull(),
  image: text('image'),
  createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
  updatedAt: timestamp('updated_at').$defaultFn(() => new Date()).notNull()
})

export const session = pgTable("session", { /* ... */ })
export const account = pgTable("account", { /* ... */ })
export const verification = pgTable("verification", { /* ... */ })
export const jwks = pgTable("jwks", { /* ... */ })
Do not modify the Better Auth tables (user, session, account, verification, jwks) unless you know what you’re doing. These are managed by Better Auth.

Adding Custom Tables

Add your own tables to schema.ts:
src/db/schema.ts
export const post = pgTable("post", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").$defaultFn(() => new Date()).notNull(),
})
Then push to database:
npm run db:push       # Quick dev push (no migration files)
# OR
npm run db:generate   # Generate migration
npm run db:migrate    # Apply migration

Migrations (src/db/migrations/)

SQL migration files are generated by Drizzle Kit when you run npm run db:generate.

Key Configuration Files

Root Directory

FilePurpose
drizzle.config.tsDrizzle ORM configuration
.env.exampleEnvironment variable template
package.jsonDependencies and scripts
next.config.tsNext.js configuration
tailwind.config.tsTailwind CSS configuration
tsconfig.jsonTypeScript configuration

Environment Variables

See Getting Started for the complete list of required environment variables.

Routing Structure

Next.js App Router automatically creates routes based on folder structure:
RouteFileDescription
/app/page.tsxHome page
/loginapp/login/page.tsxLogin page
/signupapp/signup/page.tsxRegistration page
/api/auth/*app/api/auth/[...all]/route.tsBetter Auth endpoints
/api/*app/api/[...path]/route.tsJWT-injecting proxy
Learn more about App Router in the Next.js documentation.

Build docs developers (and LLMs) love