Authentication

indiegoon uses Better Auth — a TypeScript-first auth library that handles everything from email/password to 2FA without external services.

What's Included

Out of the box, you get:

  • Email + Password — with email verification
  • OAuth — Google, GitHub, Twitter (add any provider)
  • Magic Links — passwordless sign-in via email
  • Two-Factor Auth — TOTP with 10 backup codes
  • Session Management — 30-day sessions, auto-refresh
  • Password Reset — secure token-based flow

How It Works

Auth is configured in two files:

  • src/lib/auth/index.ts — Server config (providers, plugins, hooks)
  • src/lib/auth/client.ts — Client exports (signIn, signUp, useSession)

The catch-all API route at /api/auth/[...all] handles all auth endpoints automatically.

Configuration

Basic Setup

The CLI handles this for you (goon setup auth), but here's what's configured:

// src/lib/auth/index.ts
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { db } from "@/lib/db"

export const auth = betterAuth({
  database: drizzleAdapter(db),
  session: {
    expiresIn: 60 * 60 * 24 * 30,  // 30 days
    updateAge: 60 * 60 * 24,        // refresh daily
  },
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    minPasswordLength: 8,
  },
})

Adding OAuth Providers

Set the env vars and they're automatically enabled:

# Google OAuth
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-secret"

# GitHub OAuth
GITHUB_CLIENT_ID="your-client-id"
GITHUB_CLIENT_SECRET="your-secret"

# Twitter OAuth
TWITTER_CLIENT_ID="your-client-id"
TWITTER_CLIENT_SECRET="your-secret"

Each provider is conditionally registered — if the env vars exist, the provider is active. No code changes needed.

Magic links are enabled by default with a 15-minute expiry:

import { magicLink } from "better-auth/plugins"

plugins: [
  magicLink({
    expiresIn: 60 * 15, // 15 minutes
  }),
]

Enabling Two-Factor Auth

TOTP-based 2FA with backup codes:

import { twoFactor } from "better-auth/plugins"

plugins: [
  twoFactor({
    issuer: "My SaaS",
    totpBackupCodes: 10,
  }),
]

Using Auth in Your Code

Server-Side (API Routes, Server Components)

import { auth } from "@/lib/auth"

// In an API route
export async function GET(request: Request) {
  const session = await auth.api.getSession({
    headers: request.headers,
  })

  if (!session) {
    return Response.json({ error: "Unauthorized" }, { status: 401 })
  }

  return Response.json({ user: session.user })
}

Client-Side (React Components)

"use client"

import { useSession, signOut } from "@/lib/auth/client"

export function UserMenu() {
  const { data: session, isPending } = useSession()

  if (isPending) return <div>Loading...</div>
  if (!session) return <a href="/sign-in">Sign In</a>

  return (
    <div>
      <p>Hey, {session.user.name}</p>
      <button onClick={() => signOut()}>Sign Out</button>
    </div>
  )
}

Sign In Programmatically

import { signIn } from "@/lib/auth/client"

// Email + Password
await signIn.email({ email, password, callbackURL: "/dashboard" })

// OAuth
await signIn.social({ provider: "google", callbackURL: "/dashboard" })

// Magic Link
await signIn.magicLink({ email, callbackURL: "/dashboard" })

Middleware (Route Protection)

The middleware at src/middleware.ts protects authenticated routes:

// Public paths (no auth required)
const publicPaths = [
  "/", "/sign-in", "/sign-up",
  "/forgot-password", "/pricing",
  "/blog", "/terms", "/privacy",
  "/api/auth", "/api/webhooks",
]

// Everything else requires a valid session
// Redirects to /sign-in with callbackUrl

The middleware also enforces rate limiting (20 requests per 10 seconds per IP) on all API routes except webhooks.

Auth Pages

Pre-built pages are included at:

  • /sign-in — Email/password + magic link toggle + OAuth buttons
  • /sign-up — Registration with password strength meter
  • /forgot-password — Request password reset
  • /reset-password — Set new password from token
  • /verify-email — Email verification confirmation

All pages are styled and functional. Customize the copy, add fields, or restyle them to match your brand.

Email Templates

Auth emails (verification, reset, welcome, magic link) are defined in src/lib/email/templates/auth.ts. They use a responsive HTML base template with your branding.

Session Schema

Auth data is stored in four tables (auto-created by db:push):

TablePurpose
usersUser profiles (name, email, image)
sessionsActive sessions (token, expiry, IP, user agent)
accountsOAuth accounts + credentials (multi-provider)
verificationsEmail verification + password reset tokens

Next Steps

  • Payments — Add subscriptions to your authenticated users
  • Teams — Let users create and manage organizations