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)
- 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:
import { betterAuth } from "better-auth/minimal"
import { nextCookies } from "better-auth/next-js"
import { admin, magicLink, oneTimeToken } from "better-auth/plugins"
export const auth = betterAuth({
database: prismaAdapter(db, { provider: "postgresql" }),
secondaryStorage: redis
? {
get: async key => { /* ... */ },
set: async (key, value, ttl) => { /* ... */ },
delete: async key => { /* ... */ },
}
: undefined,
socialProviders: {
google: {
clientId: env.AUTH_GOOGLE_ID,
clientSecret: env.AUTH_GOOGLE_SECRET,
},
},
session: {
freshAge: 0,
cookieCache: { enabled: true },
},
account: {
accountLinking: { enabled: true },
},
onAPIError: {
onError: error => { /* ... */ },
},
plugins: [
magicLink({
sendMagicLink: async ({ email, url }) => {
// Sends a magic link email using Resend
// ...
},
}),
oneTimeToken(),
admin(),
// must be the last plugin
nextCookies(),
],
})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.
"use client"
import { useMagicLink } from "~/hooks/use-magic-link"
export function LoginForm() {
const { form, handleSignIn, isPending } = useMagicLink()
// Renders a form with email input that calls handleSignIn on submit
// ...
}Social Login
Social login is supported through OAuth providers. Currently configured for Google:
"use client"
import { signIn } from "~/lib/auth-client"
export function LoginButton({ provider }: { provider: "google" | "github" }) {
const callbackURL = useAuthCallbackUrl()
const handleSignIn = () => {
signIn.social({ provider, callbackURL })
}
// ...
}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: ProxyConfig = {
matcher: ["/admin/:path*", "/dashboard/:path*", "/auth/:path*", "/submit"],
}Middleware auth check should not be the only protection for your routes. You should also protect routes in the route handler.
Route protection is handled entirely in the middleware. The middleware checks for a session cookie and redirects unauthenticated users to the login page, and also verifies admin role for admin routes:
const userProtectedPaths = ["/dashboard", "/submit"]
const adminProtectedPaths = ["/admin"]
const allProtectedPaths = [...userProtectedPaths, ...adminProtectedPaths]
const isProtectedPage = allProtectedPaths.some(path =>
pathname.startsWith(path),
)
if (!sessionCookie && isProtectedPage) {
return NextResponse.redirect(
new URL(`/auth/login?next=${pathname}${search}`, req.url),
)
}
if (sessionCookie && isAdminPage) {
const session = await auth.api.getSession({ headers: req.headers })
if (session?.user.role !== "admin") {
return NextResponse.redirect(new URL("/", req.url))
}
}Action Protection
Dirstarter uses oRPC for type-safe server procedures.
There are several predefined middleware layers in lib/orpc.ts that can be used
to protect actions:
withAuth: Requires an authenticated user sessionwithAdmin: Requires the user to have theadminrolewithRateLimit: Adds rate limiting with optional authwithAuthRateLimit: Combines authentication and rate limiting
import { ORPCError, os } from "@orpc/server"
import { getServerSession } from "~/lib/auth"
// withBase — injects db and revalidate into context
export const withAuth = withBase.use(async ({ next }) => {
const session = await getServerSession()
if (!session?.user) {
throw new ORPCError("UNAUTHORIZED", {
message: "User not authenticated",
})
}
return next({ context: { user: session.user } })
})
// withAdmin — extends withAuth, checks user.role === "admin"You can also create your own middleware layers and compose them. For more information on how to create and use procedures, see the oRPC documentation.
To use the middleware in your server procedures, chain .input() and
.handler() on the desired middleware:
import { withAuthRateLimit } from "~/lib/orpc"
const submit = withAuthRateLimit("submission")
.input(
z.object({
name: z.string().min(1),
websiteUrl: z.url().min(1),
submitterNote: z.string().max(256),
newsletterOptIn: z.boolean().optional().default(true),
}),
)
.handler(async ({ input, context: { user, db } }) => {
// Creates a new tool submission in the database
// ...
})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:
type EmailMagicLinkProps = { url: string; email: string }
export function EmailMagicLink({ url, email }: EmailMagicLinkProps) {
// Renders email with magic link button using EmailWrapper
// ...
}Always implement proper authorization checks on both the client and server to secure your application.
Last updated on