A production-ready Next.js 16 authentication starter with email/password, OAuth, email verification, and account management — all wired up and ready to deploy.
Features
- Email/password signup and login with required email verification
- Password reset via email
- Google OAuth
- Protected routes with server-side session validation
- Account settings: update name, change password, view and revoke sessions
- Rate limiting on auth endpoints
- Console email fallback for zero-config local development
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 16.2.6 (App Router, Turbopack) |
| Language | TypeScript |
| Auth | Better Auth 1.6.12 |
| Database | Drizzle ORM + Neon serverless Postgres |
| Resend + React Email | |
| UI | shadcn/ui (Base UI) + Tailwind CSS v4 |
| Forms | react-hook-form + Zod |
| Env validation | @t3-oss/env-nextjs |
| Testing | Vitest |
Quick Start
-
Clone the repository
git clone <repo-url> cd better-auth-starter -
Install dependencies
pnpm install -
Copy the environment file
# macOS / Linux cp .env.example .env # Windows copy .env.example .env -
Fill in your environment variables — see Environment Variables below.
-
Create a Neon database
- Go to neon.tech and create a new project.
- Do not enable "Neon Auth" — Better Auth handles authentication; only the raw Postgres connection string is needed.
- Copy the connection string into
DATABASE_URL.
-
Push the database schema
pnpm drizzle-kit push -
Start the dev server
pnpm devThe app is available at http://localhost:3000.
Environment Variables
| Variable | Required | Description | Where to get it |
|---|---|---|---|
DATABASE_URL |
Yes | Neon Postgres connection string | neon.tech |
BETTER_AUTH_SECRET |
Yes | Random secret for signing sessions. Generate: openssl rand -base64 32 |
Generate locally |
BETTER_AUTH_URL |
Yes | Base URL of the app (http://localhost:3000 in dev) |
— |
NEXT_PUBLIC_APP_URL |
Yes | Same base URL, exposed to the browser | — |
RESEND_API_KEY |
No (dev) / Yes (prod) | Resend API key for sending emails | resend.com |
EMAIL_FROM |
Yes | Sender address on a domain verified in Resend | Your verified Resend domain |
GOOGLE_CLIENT_ID |
No* | Google OAuth client ID | Google Cloud Console |
GOOGLE_CLIENT_SECRET |
No* | Google OAuth client secret | Google Cloud Console |
* The Google OAuth vars are optional: if unset, the provider is skipped and the "Continue with Google" button simply won't work until they're added. Email/password auth works without them.
Getting OAuth Credentials
- Open Google Cloud Console and create or select a project.
- Go to APIs & Services → Credentials → Create Credentials → OAuth client ID.
- Choose Web application.
- Under Authorized redirect URIs, add:
http://localhost:3000/api/auth/callback/googlehttps://your-domain.com/api/auth/callback/google(production)
- Under Authorized JavaScript origins, add
http://localhost:3000(and your production origin). - Copy the Client ID and Client Secret into
.env.
Email (Resend)
In development, if RESEND_API_KEY is not set, verification and password-reset links are printed to the server console. You can complete the full auth flow locally without a Resend account.
For production (or to test real email delivery in dev):
- Create an account at resend.com.
- Generate an API key and add it to
RESEND_API_KEY. - In Resend, go to Domains → Add Domain, add your domain, and create the DNS records it shows (SPF/DKIM). Once verified, set
EMAIL_FROMto an address on that domain (e.g.noreply@yourdomain.com). The sandbox addressonboarding@resend.devworks for quick testing but only delivers to your own Resend account email.
Available Scripts
| Script | Description |
|---|---|
pnpm dev |
Start the development server with Turbopack |
pnpm build |
Build for production |
pnpm start |
Start the production server |
pnpm test |
Run the test suite with Vitest |
pnpm drizzle-kit push |
Push the Drizzle schema to the database |
Project Structure
src/
├── app/
│ ├── (auth)/ # Public auth routes
│ │ ├── signup/
│ │ ├── login/
│ │ ├── forgot-password/
│ │ ├── reset-password/
│ │ └── verify-email/
│ ├── (dashboard)/ # Protected routes
│ │ ├── dashboard/
│ │ └── settings/
│ ├── api/auth/[...all]/ # Better Auth catch-all handler
│ ├── layout.tsx
│ └── page.tsx # Landing page
├── components/
│ ├── auth/ # Auth form components
│ ├── settings/ # Account settings components
│ └── ui/ # shadcn/ui primitives
├── lib/
│ ├── auth.ts # Better Auth server config
│ ├── auth-client.ts # Better Auth browser client
│ ├── get-session.ts # requireSession() server helper
│ ├── db/
│ │ ├── index.ts # Drizzle + Neon client
│ │ └── schema.ts # user / session / account / verification tables
│ └── email/
│ ├── resend.ts # Send helper with console fallback
│ └── templates/ # React Email templates
├── env.ts # @t3-oss/env-nextjs schema
└── proxy.ts # Next.js middleware (optimistic redirects)Deployment (Vercel)
-
Push the repository to GitHub.
-
Import the project in Vercel.
-
Add all environment variables from
.env.examplein the Vercel project settings. -
Set
BETTER_AUTH_URLandNEXT_PUBLIC_APP_URLto your production domain (e.g.https://your-domain.com). -
Update the OAuth callback URL in Google Cloud Console to include the production URL.
-
Push the schema to the production database:
# Point DATABASE_URL at the production Neon connection string, then: pnpm drizzle-kit pushAlternatively, add
pnpm drizzle-kit pushas a build command in Vercel so it runs on every deploy.
Troubleshooting
pnpm.overrides for better-call and kysely
package.json pins better-call@1.3.5 and kysely@0.28.17 via pnpm.overrides. These are required for Better Auth 1.6.12 to build correctly. Do not remove or change these overrides; doing so will cause build failures.
Emails not sending in development
If RESEND_API_KEY is not set, the app intentionally falls back to logging email links to the server console (stdout). Check your terminal output for verification and password-reset URLs. This is expected behaviour — no action needed for local dev.
NEXT_PUBLIC_APP_URL vs BETTER_AUTH_URL
Both must be set to the same base URL. BETTER_AUTH_URL is used server-side by Better Auth; NEXT_PUBLIC_APP_URL is the browser-accessible equivalent used in client-side redirect logic.