Logo ready-to-use-auth
v1.0.4

ready-to-use-auth

A production-ready Next.js 16 authentication starter powered by Better Auth. Clone it, configure your DB and providers, and have a fully working auth layer in minutes — then build your app on top. Every part is customizable — database adapter, OAuth providers, UI shell, and redirect paths — without touching auth logic.

Stack: Next.js 16 App Router · Better Auth ^1.2 · shadcn/ui · Tailwind CSS v4 · TypeScript (strict, zero any)

Project Structure

proxy.ts                       # Route protection — replaces middleware.ts in Next.js 16+

app/
  page.tsx                     # Landing page
  sign-in/page.tsx             # Sign-in route — RSC wrapper
  sign-up/page.tsx             # Sign-up route — RSC wrapper
  dashboard/
    page.tsx                   # Protected page — RSC with server-side session check
  api/auth/[...all]/route.ts   # Better Auth catch-all handler

components/auth/
  providers.ts                 # All 10 OAuth providers — flip `enabled` to toggle
  microsoft-icon.tsx           # Custom SVG icon (template for new providers)
  sign-out-button.tsx          # Client component — only piece that needs the browser
  sign-in/
    sign-in-form.tsx           # Pure UI — zero logic, receives typed props only
    sign-in-logic.tsx          # Controller — state, validation, auth calls
    index.ts                   # Re-exports as <SignIn>
  sign-up/
    sign-up-form.tsx           # Pure UI
    sign-up-logic.tsx          # Controller
    index.ts                   # Re-exports as <SignUp>

lib/
  auth.ts                      # Server-side Better Auth instance
  auth-client.ts               # Client-side auth client
  db.ts                        # Database connection — 8 options, pick one

types/
  auth.ts                      # All shared types

Quick Start

# 1. Install
pnpm install

# 2. Copy env template
cp .env.example .env.local

# 3. Fill in .env.local — minimum required:
#    BETTER_AUTH_SECRET=<run: openssl rand -base64 32>
#    NEXT_PUBLIC_APP_URL=http://localhost:3000
#    DATABASE_URL=<your connection string>

# 4. Generate schema + run migrations (default: Prisma + PostgreSQL)
npx @better-auth/cli@latest generate
npx prisma migrate dev

# 5. Start
pnpm dev

Visit http://localhost:3000 — landing page with Sign In / Create Account buttons.

How It Works

Every auth form is split into two files:

LayerFileResponsibility
UIsign-*-form.tsxPure presentational. Receives typed props. Zero auth logic, zero router calls.
Logicsign-*-logic.tsx"use client". State, zod validation, authClient calls. Passes everything to UI.

This means you can replace the entire UI without touching auth logic, or reuse the same logic with a completely different design.

Customization

1. Database

Two files work together — keep the same numbered option active in both:

#AdapterEngineMigration
1PrismaPostgreSQL (default)@better-auth/cli generate → prisma migrate dev
2PrismaMySQL / SQLite / CockroachDBsame
3Drizzle ORMpg / MySQL / SQLite@better-auth/cli generate → drizzle-kit migrate
4MongoDBMongoDBnone (schema-less)
5Kysely directPostgreSQL@better-auth/cli migrate
6Kysely directMySQL@better-auth/cli migrate
7Kysely directSQLite@better-auth/cli migrate
8Kysely directBun SQLite@better-auth/cli migrate

Each option is fully documented as comments in lib/db.ts and lib/auth.ts. Change one block in each file and run the migration.

Build script: The default build command in package.json is "prisma generate && next build". This is Prisma-specific (options 1–2). If you switch to a different adapter, update the build script accordingly:
AdapterBuild script
Prisma (1–2)"prisma generate && next build" (default)
Drizzle (3)"drizzle-kit generate && next build"
MongoDB (4)"next build"
Kysely (5–8)"next build"

Prisma and Drizzle need a code-generation step before the build so the ORM client is available at compile time. MongoDB and Kysely don't require code generation.

2. Toggle Providers On/Off

All 10 providers live in components/auth/providers.ts. Each has an enabled boolean:

{
  id: "discord",
  label: "Discord",
  icon: SiDiscord,
  brandColor: "#5865F2",
  enabled: true, // flip to show in UI
}

Then add env vars and register the redirect URI — that's it. Every page using <SignIn> or <SignUp> picks up the change automatically.

ProvideridDefault
Googlegoogle✅ enabled
GitHubgithub✅ enabled
Microsoftmicrosoft✅ enabled
Appleapple✅ enabled
Facebookfacebook✅ enabled
LinkedInlinkedin✅ enabled
Discorddiscorddisabled
Twitter / Xtwitterdisabled
Twitchtwitchdisabled
Spotifyspotifydisabled

3. Override Providers Per-Page

Both <SignIn> and <SignUp> accept an optional providerOverrides prop that lets you show/hide or rename providers for that specific page only — without changing providers.ts:

// app/sign-in/page.tsx
import { SignIn } from "@/components/auth/sign-in";

export default function SignInPage() {
  return (
    <main className="flex min-h-svh items-center justify-center p-4">
      <SignIn
        providerOverrides={{
          // Enable Discord just on this page
          discord: { enabled: true, label: "Continue with Discord" },
          // Hide Facebook on this page
          facebook: { enabled: false },
          // Rename the Google button
          google: { label: "Sign in with Google Workspace" },
        }}
      />
    </main>
  );
}

providerOverrides is typed as Partial<Record<SocialProvider, { enabled?: boolean; label?: string }>> — specify only what you want to change.

4. Change Redirect Paths

Both components accept a callbackConfig prop to override where users go after auth:

// Sign-In: redirect to /admin after login
<SignIn
  callbackConfig={{
    callbackURL: "/admin",
    errorCallbackURL: "/sign-in?error=true",
  }}
/>

// Sign-Up: different redirect for new OAuth users
<SignUp
  callbackConfig={{
    callbackURL: "/dashboard",
    newUserCallbackURL: "/onboarding",   // first-time OAuth users
    errorCallbackURL: "/sign-up",
  }}
/>
FieldTypeDefaultDescription
callbackURLstring"/dashboard"Redirect on successful auth
errorCallbackURLstringCurrent pageRedirect on auth error
newUserCallbackURLstringcallbackURLRedirect for first-time OAuth users (sign-up only)

5. Page Layout and Copy

app/sign-in/page.tsx and app/sign-up/page.tsx are plain RSCs — edit headings, descriptions, and footer links directly there. They are intentionally minimal.

6. Add a Custom Provider Icon

Create any component that satisfies AuthIconComponent (a React.ComponentType<IconProps> — receives size, className, style).

A ready-made template is at components/auth/microsoft-icon.tsx. Copy it, rename it, paste your SVG paths:

// components/auth/my-icon.tsx
import type { IconProps } from "@/types/auth";

export function MyIcon({ size = 16, className, style }: IconProps) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      className={className}
      style={style}
      aria-hidden="true"
    >
      <path d="..." />
    </svg>
  );
}

Register it in providers.ts:

import { MyIcon } from "@/components/auth/my-icon";

{ id: "spotify", label: "Spotify", icon: MyIcon, brandColor: "#1DB954", enabled: true }

If the provider ID doesn't exist in the SocialProvider union yet, add it to types/auth.ts and register the server-side config in lib/auth.ts under socialProviders.

7. Swap the Entire UI Shell

The UI components (sign-in-form.tsx, sign-up-form.tsx) are purely presentational — they only receive typed props and fire typed callbacks.

To replace the UI entirely: create a new component that accepts the same prop interface, then update one import line in the logic file.

// components/auth/sign-in/sign-in-logic.tsx
import { SignInFormUI } from "./my-sign-in-form"; // ← change this import

Your new UI inherits all state, validation, loading states, and auth calls automatically.

8. Style the Form Wrapper

Both <SignIn> and <SignUp> accept an optional className prop that is forwarded directly to the outermost div of the form. Use it to add borders, shadows, padding, max-width constraints, or any other Tailwind utilities — without touching the form components.

// Wrap the form in a card
<SignIn className="rounded-xl border bg-card p-8 shadow-md" />

// Constrain width on a wide layout
<SignUp className="mx-auto w-full max-w-sm" />

// Combine with a custom background
<SignIn className="rounded-2xl bg-linear-to-b from-muted/40 to-muted/10 p-10" />

The classes are merged with the component's default flex w-full flex-col gap-6, so layout and spacing are preserved unless you explicitly override them.

9. Theme Presets

The design tokens are defined as CSS custom properties in app/globals.css. Two ready-to-use alternative palettes are included at the bottom of that file as commented-out blocks — Ocean Blue and Forest Green. Each palette is a complete light + dark pair.

To switch themes, replace the active :root and .dark blocks at the top of globals.css with the palette of your choice:

/* In app/globals.css — replace the :root block: */
:root {
  --primary: oklch(0.488 0.243 264.376);
  /* ... rest of Ocean Blue palette */
}

.dark {
  --primary: oklch(0.6 0.2 264.376);
  /* ... rest of Ocean Blue dark palette */
}
NameHueBest for
Default (warm)~49°General purpose, neutral warmth (active)
Ocean Blue~264°SaaS, developer tools, productivity apps
Forest Green~155°Wellness, sustainability, nature products

All three presets use the same token names, so every component — forms, buttons, inputs, alerts — picks up the new palette automatically. No component changes needed.

To create your own palette, edit the CSS variable values in :root and .dark. The tokens follow the shadcn/ui convention, so any shadcn palette generator produces a drop-in replacement.

10. Dark Mode

Dark mode is active out of the box via next-themes. The ThemeProvider in app/layout.tsx uses attribute="class". All components use Tailwind dark: variants. System theme is used by default.

"use client";
import { useTheme } from "next-themes";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      Toggle
    </button>
  );
}

11. Landing Page

The default landing page lives entirely in components/landing-default/. The root route (app/page.tsx) is a thin wrapper that just imports it — making it trivial to replace.

// app/page.tsx
import { MyLandingPage } from "@/components/my-landing";

export default function HomePage() {
  return <MyLandingPage />;
}

Version badge (components/landing-default/version-badge.tsx) reads the version dynamically from package.json at build time — no manual updates needed.

12. Demo / Showcase Mode

The project includes a demo mode that lets you deploy it as a public showcase on Vercel (or anywhere) without a database. Visitors can sign in with hardcoded demo credentials and explore the dashboard, but cannot create real accounts.

How Demo Mode Works

ComponentNormal modeDemo mode
Sign-inBetter Auth + DBValidates against DEMO_EMAIL / DEMO_PASSWORD env vars
Sign-upBetter Auth + DBShows an info notice (registration disabled)
DashboardReads session from DBReads session from a lightweight cookie
Social providersShownHidden (no DB to store OAuth accounts)
MiddlewareCalls auth.api.getSessionChecks for the demo_session cookie

Demo Environment Variables

# Set these three on Vercel (or in .env.local for local testing)
NEXT_PUBLIC_DEMO_MODE=true
DEMO_EMAIL=demo@example.com
DEMO_PASSWORD=Demo1234!

DATABASE_URL and BETTER_AUTH_SECRET can be left empty — they are never used at runtime in demo mode.

Demo Mode Files

FilePurpose
lib/demo-session.tsCookie helpers + isDemoMode() / getDemoSession()
app/api/demo-signin/route.tsValidates demo creds, issues demo_session cookie
app/api/demo-signout/route.tsClears the cookie
proxy.tsBypasses Better Auth in demo mode
app/dashboard/page.tsxReads cookie session instead of hitting DB
sign-in-logic.tsxPOSTs to /api/demo-signin
sign-up-logic.tsxRenders showcase notice instead of form
sign-out-button.tsxCalls /api/demo-signout

Disabling Demo Mode (After Cloning)

To switch to the real Better Auth flow, just remove NEXT_PUBLIC_DEMO_MODE from your environment (or set it to "false"). Then set up your database and auth secrets as described in the Quick Start section above. No code changes needed — every component checks NEXT_PUBLIC_DEMO_MODE at runtime and falls back to the normal Better Auth path automatically.

# 1. Remove (or set to "false") the demo flag
NEXT_PUBLIC_DEMO_MODE=false   # or just delete the line

# 2. Set up your real auth secrets
BETTER_AUTH_SECRET=<run: openssl rand -base64 32>
DATABASE_URL=postgresql://user:password@host:5432/dbname

# 3. Run migrations
npx prisma migrate dev

# 4. Deploy — everything uses Better Auth automatically

Reading the Session

Client component

"use client";
import { authClient } from "@/lib/auth-client";

export function UserBadge() {
  const { data: session } = authClient.useSession();
  if (!session) return null;
  return <span>{session.user.name}</span>;
}

Server component (recommended for protected pages)

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function ProtectedPage() {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session) redirect("/sign-in");
  return <p>Hello, {"{session.user.name}"}</p>;
}
Tip: prefer server-side session checks for protected pages — they avoid the redirect flash that client-side useEffect guards produce. For protecting multiple routes at once, use proxy.ts at the project root (Next.js 16+ replaces middleware.ts with proxy.ts).

Type Reference

All types are in types/auth.ts — zero any across the entire codebase.

TypeDescription
SocialProviderUnion: "google" | "github" | "discord" | ...
ProviderConfig{ id, label, icon, brandColor, enabled }
AuthIconComponentReact.ComponentType<IconProps>
IconProps{ size?, className?, style?, "aria-hidden"? }
SignUpFormValues{ name, email, password, confirmPassword }
SignInFormValues{ email, password, rememberMe }
AuthError{ message: string; code?: string }
AuthCallbackConfig{ callbackURL, errorCallbackURL, newUserCallbackURL? }
SignUpFieldErrorsPartial<Record<keyof SignUpFormValues, string>>
SignInFieldErrorsPartial<Record<keyof SignInFormValues, string>>
SignUpFormPropsFull prop interface for the sign-up UI
SignInFormPropsFull prop interface for the sign-in UI

OAuth Redirect URIs

For each provider you enable, register this callback URL in their developer console:

{NEXT_PUBLIC_APP_URL}/api/auth/callback/{provider-id}

Examples:

http://localhost:3000/api/auth/callback/google
http://localhost:3000/api/auth/callback/github
http://localhost:3000/api/auth/callback/microsoft
http://localhost:3000/api/auth/callback/apple
http://localhost:3000/api/auth/callback/facebook
http://localhost:3000/api/auth/callback/linkedin