Skip to content

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

  1. User clicks on “Login” button
  2. User enters email on /login
  3. System sends a 6-digit code to their email
  4. User enters the code
  5. Session is created

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 SESSION_COOKIE_SECRET:

    Terminal window
    bun generate:secret | bunx wrangler secret put SESSION_COOKIE_SECRET --env=preview
    Terminal window
    bun generate:secret | bunx wrangler secret put SESSION_COOKIE_SECRET --env=production
  3. Add OTP_SECRET:

    Terminal window
    bun generate:secret | bunx wrangler secret put OTP_SECRET --env=preview
    Terminal window
    bun generate:secret | bunx wrangler secret put OTP_SECRET --env=production
  4. (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
FunctionModulePurpose
requireUserId()@/authServer function - returns user ID or redirects to /login
getUserId(session)@/authReturns user ID from session (no redirect)
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)
// ...
})
  • Duration: 7 days
  • OTP expiration: 5 minutes
  • Cookie: httpOnly, secure, sameSite: strict