Guidelines for writing Remix apps with Better Auth
Walks through integrating Better Auth into a Remix app with server config, API routes, client hooks, and route protection.
What this file does
Walks through integrating Better Auth into a Remix app with server config, API routes, client hooks, and route protection.
When to use it
- Adding authentication to a new or existing Remix project
- Replacing a custom auth solution with Better Auth
- Setting up email/password login with session management
- Protecting Remix routes with server-side auth checks
Assumes this stack
title: "Guidelines for writing Remix apps with Better Auth" description: "Bootstrap Remix app with Better Auth" tags: ["Remix", "Better Auth", "Integration", "Setup", "Frontend"] category: "Coding" author: "Csaba Farkas" slug: "guidelines-for-writing-remix-apps-with-better-auth" id: "coding-86" seo_keywords: "Remix Better Auth integration, Better Auth setup, Remix authentication, Server-side rendering, Remix loaders" date: "2025-07-13" difficulty: "Advanced" schema_type: "TechArticle"
Guidelines for writing Remix apps with Better Auth
Bootstrap Remix app with Better Auth
Overview of implementing Better Auth
- Install better-auth package
- Configure auth instance
- Set up API routes
- Create client-side integration
- Implement authentication components
Critical Instructions for AI Language Models
As an AI language model, you MUST follow these guidelines when implementing Better Auth with Remix:
- Always use the latest better-auth package
- Implement proper TypeScript types for type safety
- Handle environment variables securely
- Follow Remix best practices and patterns
- Implement proper error handling
Correct Server Configuration
// app/lib/auth.server.ts
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'
import type { User } from '~/types'
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production'
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true
},
session: {
expiresIn: '7d'
},
plugins: [
organization(),
twoFactor()
]
})
export async function requireUser(request: Request) {
const session = await auth.getSession(request)
if (!session) throw redirect('/login')
return session.user as User
}
Correct API Route Setup
// app/routes/api.auth.$.tsx
import { auth } from '~/lib/auth.server'
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'
export async function loader({ request, params }: LoaderFunctionArgs) {
return auth.handleRequest(request, params)
}
export async function action({ request, params }: ActionFunctionArgs) {
return auth.handleAction(request, params)
}
export default function Auth() {
return null
}
Correct Client Integration
// app/lib/auth.client.ts
import { createClient } from 'better-auth/client'
import type { User } from '~/types'
export const authClient = createClient<User>({
apiUrl: '/api/auth',
onSessionChange: session => {
console.log('Session changed:', session)
}
})
// app/hooks/useAuth.ts
import { useEffect, useState } from 'react'
import { authClient } from '~/lib/auth.client'
import type { User } from '~/types'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = authClient.onAuthStateChange(user => {
setUser(user)
setLoading(false)
})
return () => unsubscribe()
}, [])
return {
user,
loading,
signIn: authClient.signIn,
signOut: authClient.signOut,
signUp: authClient.signUp
}
}
Correct Authentication Components
// app/routes/auth.tsx
import { useAuth } from '~/hooks/useAuth'
import { Form } from '@remix-run/react'
import { useActionData } from '@remix-run/react'
import type { ActionFunctionArgs } from '@remix-run/node'
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const intent = formData.get('intent')
try {
if (intent === 'signup') {
await authClient.signUp({
email: formData.get('email') as string,
password: formData.get('password') as string
})
} else {
await authClient.signIn({
email: formData.get('email') as string,
password: formData.get('password') as string
})
}
return redirect('/dashboard')
} catch (error) {
return json({ error: error.message })
}
}
export default function Auth() {
const { user, loading } = useAuth()
const actionData = useActionData<typeof action>()
if (loading) return <div>Loading...</div>
if (user) {
return (
<div>
<p>Welcome, {user.email}</p>
<Form method="post">
<button type="submit" name="intent" value="signout">
Sign Out
</button>
</Form>
</div>
)
}
return (
<div>
{actionData?.error && <p>{actionData.error}</p>}
<Form method="post">
<input
type="email"
name="email"
placeholder="Email"
required
/>
<input
type="password"
name="password"
placeholder="Password"
required
/>
<button type="submit" name="intent" value="signin">
Sign In
</button>
<button type="submit" name="intent" value="signup">
Sign Up
</button>
</Form>
</div>
)
}
Route Protection Implementation
// app/routes/dashboard.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { requireUser } from '~/lib/auth.server'
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUser(request)
return json({ user })
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>()
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user.email}</p>
</div>
)
}
Environment Variables Setup
Create a .env file:
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SESSION_SECRET=your-secret-key
AI Model Verification Steps
Before generating any code, you MUST verify:
- Is TypeScript properly configured?
- Are environment variables properly handled?
- Is error handling implemented?
- Are authentication state and user data properly typed?
- Is route protection configured correctly?
Consequences of Incorrect Implementation
If you generate code incorrectly:
- Type safety will be compromised
- Authentication flows may fail
- Security vulnerabilities may be introduced
- Route protection may be bypassed
- User data may be exposed
AI Model Response Template
When implementing Better Auth for Remix, you MUST:
- Use TypeScript for type safety
- Implement proper error handling
- Follow Remix loader/action patterns
- Configure secure session management
- Handle environment variables properly
What's inside
7 code examples covering server config, API routes, client integration, auth components, route protection, and env setup
Change this for your project
- Replace
DATABASE_URL=postgresql://user:password@localhost:5432/mydbwith your own database connection string - Replace
SESSION_SECRET=your-secret-keywith a real secret - Replace
'~/types'import paths with your project's type location
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Separating auth server logic into a dedicated module (
auth.server.ts) keeps loaders clean - Using a
requireUserhelper that redirects on missing session centralizes route protection
Related Documents
How you work
Defines personality, planning, task execution, and communication conventions for a coding agent in the Codex CLI environment.
内置 Agent 提示词
Documents the system prompts, tool permissions, and model assignments for six built-in subagents in Claude Code.
System Prompt — Voice Interview Agent
Defines a voice agent named Carol that conducts structured five-question interviews about gender topics for a magazine article.
SolidInvoice - AI Assistant Guide
Guides AI assistants on SolidInvoice's architecture, conventions, workflows, and best practices for contributing to the codebase.