Documentation

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:

.env
BETTER_AUTH_SECRET=your_secret_key // Random string
BETTER_AUTH_URL=http://localhost:3000

You 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.

.env
# Google OAuth
AUTH_GOOGLE_ID=your_google_client_id
AUTH_GOOGLE_SECRET=your_google_client_secret

Follow the BetterAuth documentation for more information where to find the clientId and clientSecret.

Basic Configuration

Authentication is configured in lib/auth.ts:

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 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.

components/web/auth/login-form.tsx
"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:

components/web/auth/login-button.tsx
"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 management
  • user: 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

User Management

Protecting Routes and Actions

Route Protection

Use middleware to protect routes:

proxy.ts
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:

proxy.ts
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 session
  • withAdmin: Requires the user to have the admin role
  • withRateLimit: Adds rate limiting with optional auth
  • withAuthRateLimit: Combines authentication and rate limiting
lib/orpc.ts
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:

server/web/tools/router.ts
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

components/auth-button.tsx
"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:

emails/magic-link.tsx
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

On this page

Join hundreds of directory builders

Build your directory, launch, earn

Don't waste time on Stripe subscriptions or designing a pricing section. Get started today with our battle-tested stack and built-in monetization features.

Get Lifetime Access