A production-ready starter template combining TanStack Start with Better Auth for authentication, Drizzle ORM for database access, and Neon PostgreSQL.
Before you begin, ensure you have:
git clone <your-repo-url>
cd tanstack-start-better-auth-starter
pnpm install
Create a .env file in the root directory:
# Database (Required)
DATABASE_URL="postgresql://user:password@host/database?sslmode=require"
# Google OAuth (Optional - remove from lib/auth.ts if not using)
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# Better Auth Secret (Required for production)
BETTER_AUTH_SECRET="your-random-secret-string"
Push the schema to your database:
pnpm drizzle-kit push
Or generate and run migrations:
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
pnpm dev
Your app is now running at http://localhost:3000
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Neon PostgreSQL connection string |
GOOGLE_CLIENT_ID |
No | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
No | Google OAuth client secret |
BETTER_AUTH_SECRET |
Production | Secret for signing tokens |
http://localhost:3000/api/auth/callback/google├── db/
│ ├── drizzle.ts # Database connection
│ └── schema.ts # Drizzle schema (users, sessions, accounts)
├── lib/
│ ├── auth.ts # Better Auth server configuration
│ ├── auth-client.ts # Better Auth client
│ └── middleware.ts # Auth middleware for protected routes
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── header.tsx # App header with auth state
│ │ ├── login-form.tsx # Login form component
│ │ └── signup-form.tsx # Signup form component
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Home page
│ │ ├── login.tsx # Login page
│ │ ├── signup.tsx # Signup page
│ │ ├── dashboard.tsx # Protected dashboard
│ │ └── api/auth/$.ts # Auth API handler
│ └── styles.css # Global styles (Tailwind)
├── drizzle.config.ts # Drizzle Kit configuration
└── components.json # shadcn/ui configuration
This starter uses Better Auth with Drizzle adapter:
lib/auth.ts configures the auth instancelib/auth-client.ts provides React hookssrc/routes/api/auth/$.ts handles all auth endpointsimport { authClient } from 'lib/auth-client'
function MyComponent() {
// Get current session
const { data: session } = authClient.useSession()
if (session) {
return
Welcome, {session.user.name}!
}
return
Please log in
}
// Email sign in
await authClient.signIn.email({
email: 'user@example.com',
password: 'password123',
callbackURL: '/dashboard',
})
// Email sign up
await authClient.signUp.email({
email: 'user@example.com',
password: 'password123',
name: 'John Doe',
callbackURL: '/dashboard',
})
// Google sign in
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
})
// Sign out
await authClient.signOut()
Use the auth middleware to protect server-side routes:
import { createFileRoute } from '@tanstack/react-router'
import { authMiddleware } from 'lib/middleware'
export const Route = createFileRoute('/protected')({
component: ProtectedPage,
server: {
middleware: [authMiddleware],
},
})
Users without a valid session will be redirected to /login.
Edit lib/auth.ts to add providers:
export const auth = betterAuth({
// ...existing config
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
},
},
})
If you don't need Google OAuth:
socialProviders section from lib/auth.tsGOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from .envUse shadcn/ui to add new components:
pnpx shadcn@latest add button
pnpx shadcn@latest add dialog
pnpx shadcn@latest add dropdown-menu
| Command | Description |
|---|---|
pnpm dev |
Start development server on port 3000 |
pnpm build |
Build for production |
pnpm serve |
Preview production build |
pnpm test |
Run tests with Vitest |
pnpm lint |
Run ESLint |
pnpm format |
Run Prettier |
pnpm check |
Format and lint fix |
pnpm drizzle-kit push |
Push schema to database |
pnpm drizzle-kit studio |
Open Drizzle Studio |
The starter includes Better Auth's required tables:
Launch Drizzle Studio to browse your database:
pnpm drizzle-kit studio
Ensure these are set in your production environment:
DATABASE_URL="your-production-database-url"
BETTER_AUTH_SECRET="a-long-random-string"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
In lib/auth-client.ts, update the baseURL for production:
export const authClient = createAuthClient({
baseURL: process.env.NODE_ENV === 'production'
? 'https://your-domain.com'
: 'http://localhost:3000'
})
pnpm build
Deploy the .output directory to your hosting provider.
Files and folders prefixed with demo contain example code showcasing various TanStack Start features (SSR modes, API requests, server functions). You can safely delete these once you're familiar with the patterns.
DATABASE_URL is correct?sslmode=require)http://localhost:3000/api/auth/callback/google/login route exists and is accessible/api/auth/$ is workingMIT
Built with ❤️ using the TanStack ecosystem
$ claude mcp add tanstack-start-better-auth-starter \
-- python -m otcore.mcp_server <graph>