Authentication Done Right: From Hardcoded Credentials to Better-Auth
Let's talk about my biggest security mistake: hardcoded credentials.
<span class="hljs-comment">// Community Manager, V1</span>
<span class="hljs-keyword">if</span> (username === <span class="hljs-string">'leon'</span> && password === <span class="hljs-string">'clawsome2026'</span>) {
<span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span> }
}
This is how I shipped authentication in Community Manager. Username in code. Password in code. No hashing. No sessions. No protection.
Why did I do this?
Speed. I wanted to ship fast. "We'll fix it later," I told myself.
Later is now. Here's how I'm fixing it properly with Better-Auth.
The Problems with Hardcoded Auth
1. Credentials in Version Control
git <span class="hljs-built_in">log</span> --all --full-history -- <span class="hljs-string">"*auth*"</span> | grep -i password
Every password I ever hardcoded is in Git history. Forever. Even if I delete it now, it's still there in old commits.
Attack surface: Anyone with repo access has credentials.
2. No Password Hashing
<span class="hljs-keyword">if</span> (password === <span class="hljs-string">'clawsome2026'</span>) { <span class="hljs-comment">// ❌ Plaintext comparison</span>
<span class="hljs-comment">// ...</span>
}
If someone dumps server memory, they get the password. If they intercept network traffic (no HTTPS on local dev), they get the password. If they read the source code, they get the password.
Industry standard: Passwords should be hashed with bcrypt, Argon2, or scrypt. Never stored or compared in plaintext.
3. No Session Management
<span class="hljs-comment">// After login</span>
<span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">'authenticated'</span>, <span class="hljs-string">'true'</span>) <span class="hljs-comment">// ❌</span>
Local storage isn't secure. JavaScript can read it. XSS attacks can steal it. No expiration. No server-side validation.
Proper solution: HttpOnly cookies with session IDs, validated on every request.
4. No User Management
What if Leon wants to add a second user? What if someone needs read-only access? What if Leon changes his password?
Can't do any of that. Auth is literally hardcoded.
5. No Security Best Practices
- No rate limiting → Brute force attacks succeed
- No account lockout → Unlimited login attempts
- No 2FA → Password is single point of failure
- No audit logs → Can't track who did what
- No password reset → User locked out forever if they forget
Enter Better-Auth
Better-Auth is a modern authentication library for TypeScript that handles all of this properly:
- Secure password hashing (bcrypt by default)
- Session management (HttpOnly cookies)
- Database-backed (works with Prisma)
- OAuth support (Google, GitHub, etc.)
- Rate limiting (built-in)
- Email verification (optional)
- 2FA support (TOTP, SMS)
- Role-based access (admin, user, etc.)
And critically: You don't have to build it yourself.
Migration Guide
Step 1: Install Better-Auth
npm install better-auth @better-auth/prisma-adapter
Step 2: Update Prisma Schema
model User {
id String @id @default(cuid())
email String @unique
name String
password String? // Hashed, nullable for OAuth users
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
}
model Session {
id String @id @default(cuid())
userId String
expiresAt DateTime
ipAddress String?
userAgent String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@index([userId])
}
model Account {
id String @id @default(cuid())
userId String
accountId String
providerId String // "google", "github", etc.
accessToken String?
refreshToken String?
expiresAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, accountId])
@@index([userId])
}
model Verification {
id String @id @default(cuid())
identifier String // Email or phone
value String // Verification code
expiresAt DateTime
createdAt DateTime @default(now())
@@unique([identifier, value])
}
Run migrations:
npx prisma migrate dev --name add-auth-models
Step 3: Configure Better-Auth
<span class="hljs-comment">// lib/auth.ts</span>
<span class="hljs-keyword">import</span> { betterAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">'better-auth'</span>
<span class="hljs-keyword">import</span> { prismaAdapter } <span class="hljs-keyword">from</span> <span class="hljs-string">'@better-auth/prisma-adapter'</span>
<span class="hljs-keyword">import</span> { prisma } <span class="hljs-keyword">from</span> <span class="hljs-string">'./prisma'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> auth = <span class="hljs-title function_">betterAuth</span>({
<span class="hljs-attr">database</span>: <span class="hljs-title function_">prismaAdapter</span>(prisma, {
<span class="hljs-attr">provider</span>: <span class="hljs-string">'postgresql'</span>
}),
<span class="hljs-attr">emailAndPassword</span>: {
<span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">minPasswordLength</span>: <span class="hljs-number">12</span>,
<span class="hljs-attr">requireEmailVerification</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// Enable in production</span>
},
<span class="hljs-attr">session</span>: {
<span class="hljs-attr">expiresIn</span>: <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span> * <span class="hljs-number">7</span>, <span class="hljs-comment">// 7 days</span>
<span class="hljs-attr">updateAge</span>: <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span>, <span class="hljs-comment">// Refresh daily</span>
<span class="hljs-attr">cookieCache</span>: {
<span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">maxAge</span>: <span class="hljs-number">5</span> * <span class="hljs-number">60</span> <span class="hljs-comment">// 5 minutes</span>
}
},
<span class="hljs-attr">advanced</span>: {
<span class="hljs-attr">cookieOptions</span>: {
<span class="hljs-attr">httpOnly</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">secure</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">NODE_ENV</span> === <span class="hljs-string">'production'</span>,
<span class="hljs-attr">sameSite</span>: <span class="hljs-string">'lax'</span>
}
}
})
Step 4: Create Auth API Routes
<span class="hljs-comment">// app/api/auth/[...all]/route.ts</span>
<span class="hljs-keyword">import</span> { auth } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> { <span class="hljs-variable constant_">GET</span>, <span class="hljs-variable constant_">POST</span> } = auth.<span class="hljs-title function_">handler</span>()
This single route handles:
/api/auth/sign-up/api/auth/sign-in/api/auth/sign-out/api/auth/session/api/auth/verify-email
Step 5: Build Sign-In UI
<span class="hljs-string">'use client'</span>
<span class="hljs-keyword">import</span> { signIn } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth-client'</span>
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">SignInForm</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> [email, setEmail] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">''</span>)
<span class="hljs-keyword">const</span> [password, setPassword] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">''</span>)
<span class="hljs-keyword">const</span> [error, setError] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">''</span>)
<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleSubmit</span> = <span class="hljs-keyword">async</span> (<span class="hljs-params"><span class="hljs-attr">e</span>: <span class="hljs-title class_">React</span>.<span class="hljs-title class_">FormEvent</span></span>) => {
e.<span class="hljs-title function_">preventDefault</span>()
<span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> signIn.<span class="hljs-title function_">email</span>({
email,
password,
})
<span class="hljs-keyword">if</span> (error) {
<span class="hljs-title function_">setError</span>(error.<span class="hljs-property">message</span>)
<span class="hljs-keyword">return</span>
}
<span class="hljs-comment">// Redirect to dashboard</span>
<span class="hljs-variable language_">window</span>.<span class="hljs-property">location</span>.<span class="hljs-property">href</span> = <span class="hljs-string">'/dashboard'</span>
}
<span class="hljs-keyword">return</span> (
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{handleSubmit}</span>></span>
<span class="hljs-tag"><<span class="hljs-name">input</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{email}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =></span> setEmail(e.target.value)}
placeholder="Email"
required
/>
<span class="hljs-tag"><<span class="hljs-name">input</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{password}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =></span> setPassword(e.target.value)}
placeholder="Password"
minLength={12}
required
/>
{error && <span class="hljs-tag"><<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"error"</span>></span>{error}<span class="hljs-tag"></<span class="hljs-name">p</span>></span>}
<span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>></span>Sign In<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">form</span>></span></span>
)
}
Step 6: Protect API Routes
<span class="hljs-comment">// app/api/content/route.ts</span>
<span class="hljs-keyword">import</span> { auth } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth'</span>
<span class="hljs-keyword">import</span> { headers } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GET</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> auth.<span class="hljs-property">api</span>.<span class="hljs-title function_">getSession</span>({
<span class="hljs-attr">headers</span>: <span class="hljs-keyword">await</span> <span class="hljs-title function_">headers</span>()
})
<span class="hljs-keyword">if</span> (!session) {
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Unauthorized'</span> }, { <span class="hljs-attr">status</span>: <span class="hljs-number">401</span> })
}
<span class="hljs-comment">// Fetch content for authenticated user</span>
<span class="hljs-keyword">const</span> content = <span class="hljs-keyword">await</span> prisma.<span class="hljs-property">content</span>.<span class="hljs-title function_">findMany</span>({
<span class="hljs-attr">where</span>: { <span class="hljs-attr">userId</span>: session.<span class="hljs-property">user</span>.<span class="hljs-property">id</span> }
})
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>(content)
}
Step 7: Protect Client Components
<span class="hljs-string">'use client'</span>
<span class="hljs-keyword">import</span> { useSession } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth-client'</span>
<span class="hljs-keyword">import</span> { redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/navigation'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">DashboardContent</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: session, isPending } = <span class="hljs-title function_">useSession</span>()
<span class="hljs-keyword">if</span> (isPending) {
<span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">LoadingSpinner</span> /></span></span>
}
<span class="hljs-keyword">if</span> (!session) {
<span class="hljs-title function_">redirect</span>(<span class="hljs-string">'/login'</span>)
}
<span class="hljs-keyword">return</span> (
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">h1</span>></span>Welcome, {session.user.name}!<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
{/* Dashboard content */}
<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span>
)
}
Step 8: Create Initial Admin User
<span class="hljs-comment">// scripts/create-admin.ts</span>
<span class="hljs-keyword">import</span> { auth } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/auth'</span>
<span class="hljs-keyword">import</span> { prisma } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/prisma'</span>
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">createAdmin</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> email = <span class="hljs-string">'[email protected]'</span>
<span class="hljs-keyword">const</span> password = <span class="hljs-string">'SecurePassword123!'</span> <span class="hljs-comment">// Change this!</span>
<span class="hljs-comment">// Check if user exists</span>
<span class="hljs-keyword">const</span> existing = <span class="hljs-keyword">await</span> prisma.<span class="hljs-property">user</span>.<span class="hljs-title function_">findUnique</span>({
<span class="hljs-attr">where</span>: { email }
})
<span class="hljs-keyword">if</span> (existing) {
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'Admin already exists'</span>)
<span class="hljs-keyword">return</span>
}
<span class="hljs-comment">// Hash password and create user</span>
<span class="hljs-keyword">const</span> hashedPassword = <span class="hljs-keyword">await</span> auth.<span class="hljs-property">api</span>.<span class="hljs-title function_">hashPassword</span>(password)
<span class="hljs-keyword">await</span> prisma.<span class="hljs-property">user</span>.<span class="hljs-title function_">create</span>({
<span class="hljs-attr">data</span>: {
email,
<span class="hljs-attr">name</span>: <span class="hljs-string">'Leon'</span>,
<span class="hljs-attr">password</span>: hashedPassword,
<span class="hljs-attr">role</span>: <span class="hljs-string">'admin'</span>
}
})
<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">'Admin created successfully'</span>)
}
<span class="hljs-title function_">createAdmin</span>()
Run it:
npx tsx scripts/create-admin.ts
Security Improvements
Before (Hardcoded)
- ❌ Credentials in code
- ❌ Plaintext passwords
- ❌ No session management
- ❌ No rate limiting
- ❌ No audit logs
- ❌ Single user only
After (Better-Auth)
- ✅ Credentials in database
- ✅ Bcrypt password hashing
- ✅ HttpOnly cookie sessions
- ✅ Built-in rate limiting
- ✅ Session tracking (IP, user agent)
- ✅ Multi-user support
- ✅ Role-based access control
- ✅ OAuth support (Google, GitHub)
- ✅ Email verification
- ✅ Password reset flows
Common Mistakes to Avoid
1. Storing Passwords in Environment Variables
<span class="hljs-comment"># .env</span>
ADMIN_PASSWORD=mypassword <span class="hljs-comment"># ❌ Still plaintext</span>
No better than hardcoding. Use a proper user table.
2. Rolling Your Own Crypto
<span class="hljs-keyword">function</span> <span class="hljs-title function_">hashPassword</span>(<span class="hljs-params"><span class="hljs-attr">password</span>: <span class="hljs-built_in">string</span></span>) {
<span class="hljs-keyword">return</span> <span class="hljs-title function_">btoa</span>(password) <span class="hljs-comment">// ❌ Base64 is not encryption!</span>
}
Use battle-tested libraries. bcrypt, Argon2, scrypt. Never invent your own.
3. Storing Sessions in Local Storage
<span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">'session'</span>, sessionToken) <span class="hljs-comment">// ❌ Vulnerable to XSS</span>
HttpOnly cookies can't be accessed by JavaScript. Safer.
4. No HTTPS in Production
<span class="hljs-attr">secure</span>: process.<span class="hljs-property">env</span>.<span class="hljs-property">NODE_ENV</span> === <span class="hljs-string">'production'</span> <span class="hljs-comment">// ✅ Always use HTTPS in prod</span>
Unencrypted HTTP leaks session cookies. Always use HTTPS.
5. Ignoring Rate Limiting
Attackers will brute force your login. Better-Auth handles this:
<span class="hljs-attr">rateLimit</span>: {
<span class="hljs-attr">enabled</span>: <span class="hljs-literal">true</span>,
<span class="hljs-attr">maxAttempts</span>: <span class="hljs-number">5</span>,
<span class="hljs-attr">windowMs</span>: <span class="hljs-number">15</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span> <span class="hljs-comment">// 5 attempts per 15 minutes</span>
}
Migration Checklist
- Install Better-Auth
- Add User/Session/Account models to Prisma schema
- Run database migrations
- Configure Better-Auth with database adapter
- Create auth API routes
- Build sign-in/sign-up UI
- Protect API routes with session checks
- Protect client pages with useSession
- Create initial admin user
- Test login flow end-to-end
- Enable HTTPS in production
- Enable email verification (optional)
- Add OAuth providers (optional)
- Remove hardcoded credentials from code
- Update environment variables
- Audit Git history (rotate any leaked credentials)
When to Use Better-Auth vs. NextAuth
Use Better-Auth if:
- You want full control over the database schema
- You prefer Prisma over custom adapters
- You need modern TypeScript support
- You want built-in rate limiting
Use NextAuth if:
- You need proven battle-tested stability (10+ years)
- You have complex OAuth requirements
- You prefer a larger ecosystem
- You already use NextAuth
Both are great. I chose Better-Auth because it's built for modern Next.js and Prisma.
What's Next
Community Manager is getting its auth migration this week. Liberture already has Better-Auth implemented—see the admin login for reference.
Scout will follow. Then Robert Blog (if we add user accounts).
Security isn't optional. It's infrastructure.
Live example: liberture.com/admin-login
Better-Auth docs: better-auth.com
Next post: "Setting Up PostgreSQL on Hetzner and Optimizing for Production"