Authentication
Learn how authentication works in Dirstarter, including BetterAuth setup, providers, user management, and security
Dirstarter uses BetterAuth for authentication, providing a flexible and secure system with multiple authentication methods and role-based access control.
Core Features
- Magic Link authentication with Resend integration
- Social login providers (Google, GitHub)
- Role-based access control (admin/user)
- Session management
- User management interface
Setup
The following environment variables are required:
BETTER_AUTH_SECRET=your_secret_key // Random string
BETTER_AUTH_URL=http://localhost:3000You can generate a random string for the secret key using openssl rand -base64 32.
For every OAuth provider you want to use, you need to set the clientId and clientSecret provider variables.
# Google OAuth
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secretFollow the BetterAuth documentation for more information where to find the clientId and clientSecret.
Basic Configuration
Authentication is configured in lib/auth.ts:
export const auth = betterAuth({
database: prismaAdapter(db, {
provider: "postgresql",
}),
socialProviders: {
google: {
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET,
},
},
account: {
accountLinking: {
enabled: true,
},
},
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
// Email sending logic
},
}),
admin(),
// Add other plugins here
],
})For a full list of available configuration options, plugins and providers see the BetterAuth documentation.
Authentication Methods
By default, Dirstarter includes 2 authentication methods: Magic Link and Google Login.
Adding Authentication Methods
BetterAuth supports a wide range of authentication methods, including more social providers, email/password, passkeys and more.
Magic Link
Magic Link authentication allows users to sign in without a password. When a user enters their email, they receive a secure link that automatically logs them in.
const handleSignIn = ({ email }: z.infer<typeof schema>) => {
signIn.magicLink({
email,
callbackURL,
fetchOptions: {
onSuccess: () => {
router.push(`/auth/verify?email=${email}`)
},
onError: ({ error }) => {
toast.error(error.message)
},
},
})
}Social Login
Social login is supported through OAuth providers. Currently configured for Google:
const handleSignIn = () => {
signIn.social({
provider,
callbackURL,
fetchOptions: {
onRequest: () => {
setIsPending(true)
},
onError: ({ error }) => {
toast.error(error.message)
},
},
})
}User Roles
Dirstarter implements two user roles:
admin: Full access to all features and user managementuser: Standard user access
User Management
Admins have access to various user management actions:
- Edit user details
- Change user roles
- Ban/unban users
- Revoke user sessions
- Delete users

Protecting Routes and Actions
Route Protection
Use middleware to protect routes:
export const config = {
matcher: ["/admin/:path*", "/auth/:path*"],
}Middleware auth check should not be the only protection for your routes. You should also protect routes in the route handler.
Protecting a page is done by checking the session in the page.
import { auth } from "~/lib/auth"
import { redirect } from "next/navigation"
import { headers } from "next/headers"
export default async function AdminPage({ children }: { children: React.ReactNode }) {
const session = await auth.api.getSession({ headers: await headers() })
if (session?.user.role !== "admin") {
redirect("/auth/login")
}
return <div>{children}</div>
}Action Protection
There are 2 predefined server actions procedures that can be used to protect actions:
userProcedure: Protects actions that require a user to be authenticatedadminProcedure: Protects actions that require a user to be an admin
export const userProcedure = createProcedure().handler(async () => {
const session = await auth.api.getSession({ headers: await headers() })
if (!session?.user) {
throw new Error("User not authenticated")
}
return { user: session.user }
})
export const adminProcedure = createProcedure(userProcedure).handler(async ({ ctx }) => {
if (ctx.user.role !== "admin") {
throw new Error("User not authorized")
}
return { user: ctx.user }
})You can also create your own procedures and protect them with the same logic. For more information on how to create and use procedures, see the Zod Safe Actions documentation.
To use the procedures in your server actions, create them with the createServerAction function from ZSA:
export const createTool = userProcedure
.createServerAction()
.input(z.object({
name: z.string(),
description: z.string(),
}))
.handler(async ({ input, ctx: { user } }) => {
// Create a new tool
const tool = await db.tool.create({
data: {
name: input.name,
description: input.description,
ownerId: user.id, // You can be certain that the user is authenticated
},
})
return tool
})Client-Side Usage
Session Management
'use client'
import { useSession, signIn, signOut } from '~/lib/auth-client'
export function AuthButton() {
const { data: session, isPending } = useSession()
if (isPending) {
return <div>Loading...</div>
}
if (session) {
return (
<button onClick={() => signOut()}>
Sign out
</button>
)
}
return (
<button onClick={() => signIn.social({ provider: 'google' })}>
Sign in with Google
</button>
)
}Admin Checks
const { data: session } = useSession()
if (session?.user.role === 'admin') {
// Show admin UI
}Email Templates
Magic link emails use React Email for beautiful, responsive templates:
const EmailLoginLink = ({ url, ...props }: EmailProps) => {
return (
<EmailWrapper {...props}>
<Text>Welcome to {config.site.name}!</Text>
<Text>Please click the magic link below to sign in to your account.</Text>
<EmailButton href={url}>Sign in to {config.site.name}</EmailButton>
<Text>or copy and paste this URL into your browser:</Text>
<Text className="max-w-sm flex-wrap break-words font-medium leading-snug">{url}</Text>
</EmailWrapper>
)
}Always implement proper authorization checks on both the client and server to secure your application.
Last updated on