Skip to content

Authentication

Built-in authentication powered by Better Auth, using OTP (One-Time Password) via email. No passwords to store or reset. Users don’t need to remember credentials. Sessions, cookies, and access control are handled by Better Auth with a TanStack Start integration.

How it works

  1. User clicks on “Login” button
  2. User enters email on /login
  3. System sends a 6-digit code to their email (via Better Auth email OTP)
  4. User enters the code
  5. Better Auth creates a session and sets cookies

For local development, all required variables are already configured in the .env file.

For production and preview environments:

  1. Set up email delivery - OTP codes are sent via email, so email delivery must be configured for authentication to work in production. Follow the Email setup guide first.

  2. Add BETTER_AUTH_SECRET:

    Terminal window
    bun generate:secret | bunx wrangler secret put BETTER_AUTH_SECRET --env=preview
    Terminal window
    bun generate:secret | bunx wrangler secret put BETTER_AUTH_SECRET --env=production
  3. (Optional) Enable Captcha - protect the login page against bots with Cloudflare Turnstile. The captcha appears automatically when configured, and always passes in local development.

    • Add a Turnstile widget in the Cloudflare dashboard
    • Set CLOUDFLARE_TURNSTILE_PUBLIC_KEY in wrangler.json under vars
    • Add CLOUDFLARE_TURNSTILE_SECRET_KEY as a secret:
    Terminal window
    bunx wrangler secret put CLOUDFLARE_TURNSTILE_SECRET_KEY --env=preview
    Terminal window
    bunx wrangler secret put CLOUDFLARE_TURNSTILE_SECRET_KEY --env=production
PieceLocationPurpose
Server instanceapps/auth/auth.server.tsBetter Auth config (email OTP, admin AC, Polar plugins, TanStack Start cookies)
Clientapps/auth/auth.tsBetter Auth React client (email OTP, admin, Polar)
Access controlapps/auth/access.tsRoles and content permissions (user, admin, premium, elite)
API catch-all/api/auth/$Handles Better Auth HTTP endpoints (sessions, OTP, Polar webhooks, checkout)
FunctionModulePurpose
requireUserId()@/authServer function - returns user ID or redirects to /login
getUserById(userId)@/authReturns user record from database
useUser()@/authClient-side hook to access current user
useLogout()@/authClient-side hook that returns an async logout function

Login and logout buttons are already included in the header (apps/brand/routes/layout.tsx).

The /login route handles the entire auth flow. It supports a redirectTo query parameter:

/login?redirectTo=/dashboard

After successful authentication, the user is redirected to the specified URL.

To sign out, use the useLogout hook:

import { useLogout } from "@/auth/hooks"
function LogoutButton() {
const logout = useLogout()
return (
<button type="button" onClick={() => void logout()}>
Sign out
</button>
)
}

Use requireUserId in beforeLoad to restrict access:

import { createFileRoute } from "@tanstack/react-router"
import { requireUserId } from "@/auth"
export const Route = createFileRoute("/dashboard")({
beforeLoad: async () => {
await requireUserId()
},
})

If not authenticated, the user is redirected to /login with a redirectTo parameter pointing back to the current page.

On the client, use the useUser hook:

import { useUser } from "@/auth"
function MyComponent() {
const { data } = useUser()
if (data?.user) {
return <p>Hello, {data.user.email}</p>
}
return <p>Not signed in</p>
}

On the server, use a server function with requireUserId and getUserById:

import { createServerFn } from "@tanstack/react-start"
import { requireUserId } from "@/auth"
import { getUserById } from "@/auth"
const getPageData = createServerFn({ method: "GET" }).handler(async () => {
const userId = await requireUserId()
const user = await getUserById(userId)
// ...
})