Authentication
Built-in authentication using OTP (One-Time Password) via email. No third-party auth services required. No passwords to store or reset. Users don’t need to remember credentials. Simple, secure by design, and works for most applications.
How it works
- User clicks on “Login” button
- User enters email on
/login - System sends a 6-digit code to their email
- User enters the code
- Session is created
For local development, all required variables are already configured in the .env file.
For production and preview environments:
-
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.
-
Add
SESSION_COOKIE_SECRET:Terminal window bun generate:secret | bunx wrangler secret put SESSION_COOKIE_SECRET --env=previewTerminal window bun generate:secret | bunx wrangler secret put SESSION_COOKIE_SECRET --env=production -
Add
OTP_SECRET:Terminal window bun generate:secret | bunx wrangler secret put OTP_SECRET --env=previewTerminal window bun generate:secret | bunx wrangler secret put OTP_SECRET --env=production -
(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_KEYinwrangler.jsonundervars - Add
CLOUDFLARE_TURNSTILE_SECRET_KEYas a secret:
Terminal window bunx wrangler secret put CLOUDFLARE_TURNSTILE_SECRET_KEY --env=previewTerminal window bunx wrangler secret put CLOUDFLARE_TURNSTILE_SECRET_KEY --env=production
| Function | Module | Purpose |
|---|---|---|
requireUserId() | @/auth | Server function - returns user ID or redirects to /login |
getUserId(session) | @/auth | Returns user ID from session (no redirect) |
getUserById(userId) | @/auth | Returns user record from database |
useUser() | @/auth | Client-side hook to access current user |
useLogout() | @/auth | Client-side hook that returns an async logout function |
Login & Logout
Section titled “Login & Logout”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=/dashboardAfter 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> )}Protecting Pages
Section titled “Protecting Pages”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.
Accessing User Data
Section titled “Accessing User Data”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) // ...})Session Details
Section titled “Session Details”- Duration: 7 days
- OTP expiration: 5 minutes
- Cookie:
httpOnly,secure,sameSite: strict