Visual Explainer · Better-Auth

A thin layer that handles the hard parts.

Two files. Ten security concerns you don't write yourself. One hour to ship.

§ 01 — Mental Model

Where it sits in your stack.

User
👤 Browser
Framework
TanStack Start
one route file: app/routes/api/auth.$.ts
Auth Layer (this thing)
Better-Auth
~50 KB · framework-agnostic · TS-first
Storage
📦 Postgres
3 tables · users · sessions · verification_tokens
Email out
✉ Resend
called from your sendVerificationEmail callback
§ 02 — How a Signup Actually Works

The full flow, top to bottom.

Sequence · Sign up → verify → logged in
sequenceDiagram
  autonumber
  actor User
  participant Browser
  participant Route as TanStack Route
  participant BA as Better-Auth
  participant DB as Postgres
  participant Mail as Resend

  User->>Browser: Fill signup form
  Browser->>Route: POST /api/auth/sign-up
  Route->>BA: auth.handler(request)

  Note over BA: Hash password (Argon2id)
  BA->>DB: INSERT user
  BA->>DB: INSERT verification_token

  BA->>Mail: send verify email
  Mail-->>User: ✉ noreply@sitzio.de

  User->>Browser: Click verify link
  Browser->>Route: GET /api/auth/verify-email
  Route->>BA: auth.handler(request)

  Note over BA: Validate token, single-use
  BA->>DB: UPDATE user verified=true
  BA->>DB: DELETE verification_token
  BA->>DB: INSERT session

  BA-->>Browser: Set-Cookie session_token
(httpOnly, Secure, SameSite) Browser-->>User: ✓ Logged in
§ 03 — What You Don't Have to Write

Ten security landmines, defused.

Argon2id hashing
Right algorithm, right parameters. Brute-force resistant.
✓ handled
CSPRNG session tokens
Cryptographically random IDs. Not Math.random().
✓ handled
httpOnly + Secure cookies
XSS can't steal the session. SameSite blocks CSRF.
✓ handled
CSRF protection
Origin checks, double-submit cookies. Built in.
✓ handled
Token expiry
Verification + reset tokens auto-expire and self-clean.
✓ handled
Single-use tokens
Reset link can't be replayed. Used = deleted.
✓ handled
Rate limiting
Login + reset endpoints throttled. Brute force = denied.
✓ handled
No account enumeration
"Email exists" doesn't leak. Same response, always.
✓ handled
Timing-safe compare
Hash comparison takes constant time. No timing attacks.
✓ handled
Session rotation
New session on login + privilege change. Session fixation = blocked.
✓ handled
§ 04 — The Real Integration

Two files. That's it.

app/lib/auth.ts ~20 lines
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'
import { resend } from './resend'

export const auth = betterAuth({
  database: new Pool({
    connectionString: process.env.DATABASE_URL
  }),

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },

  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      await resend.emails.send({
        from: 'noreply@sitzio.de',
        to: user.email,
        subject: 'Verify your email',
        html: `<a href="${url}">Verify</a>`,
      })
    },
  },
})
app/routes/api/auth.$.ts ~7 lines
// One file. All auth routes.
import { createAPIFileRoute }
  from '@tanstack/start/api'
import { auth } from '~/lib/auth'

export const Route = createAPIFileRoute(
  '/api/auth/$'
)({
  GET:  ({ request }) => auth.handler(request),
  POST: ({ request }) => auth.handler(request),
})

// That's the entire wiring.
// Better-Auth handles every
// /api/auth/* endpoint internally.
§ 05 — vs. The Alternatives

Why Better-Auth, specifically.

DIY
Setup~3 days
Code~400 lines
Securityyou own it
Maintainedby you
TanStackn/a
Auth.js
Setup~3 hours
Code~80 lines
Securityhandled
Maintainedactive
TanStackcommunity
Lucia
Setup~4 hours
Code~150 lines
Securitylow-level
Maintainedarchived
TanStackmanual
Better-Auth
Setup~1 hour
Code~30 lines
Securityhandled
Maintainedactive
TanStackfirst-class
§ 06 — The Numbers

What it saves you.

~30
Lines of code
(was ~400 DIY)
~1hr
Integration time
(was ~3 days DIY)
10
Security landmines
handled by default
2
Files to write
auth.ts + auth.$.ts
Don't write auth code at 2 AM.
Two files. One hour. Ship the niche site instead.