Files
dotfiles/dot_agents/skills/tanstack-start-best-practices/rules/auth-route-protection.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

4.5 KiB

auth-route-protection: Protect Routes with beforeLoad

Priority: HIGH

Explanation

Use beforeLoad in route definitions to check authentication before the route loads. This prevents unauthorized access, redirects to login, and can extend context with user data for child routes.

Bad Example

// Checking auth in component - too late, data may have loaded
function DashboardPage() {
  const user = useAuth()

  useEffect(() => {
    if (!user) {
      navigate({ to: '/login' })  // Redirect after render
    }
  }, [user])

  if (!user) return null  // Flash of content possible

  return <Dashboard user={user} />
}

// No protection on route
export const Route = createFileRoute('/dashboard')({
  loader: async () => {
    // Fetches sensitive data even for unauthenticated users
    return await fetchDashboardData()
  },
  component: DashboardPage,
})

Good Example: Route-Level Protection

// routes/_authenticated.tsx - Layout route for protected area
import { createFileRoute, redirect, Outlet } from '@tanstack/react-router'
import { getSessionData } from '@/lib/session.server'

export const Route = createFileRoute('/_authenticated')({
  beforeLoad: async ({ location }) => {
    const session = await getSessionData()

    if (!session) {
      throw redirect({
        to: '/login',
        search: {
          redirect: location.href,
        },
      })
    }

    // Extend context with user for all child routes
    return {
      user: session,
    }
  },
  component: AuthenticatedLayout,
})

function AuthenticatedLayout() {
  return (
    <div>
      <AuthenticatedNav />
      <main>
        <Outlet />  {/* Child routes render here */}
      </main>
    </div>
  )
}

// routes/_authenticated/dashboard.tsx
// This route is automatically protected by parent
export const Route = createFileRoute('/_authenticated/dashboard')({
  loader: async ({ context }) => {
    // context.user is guaranteed to exist
    return await fetchDashboardData(context.user.id)
  },
  component: DashboardPage,
})

function DashboardPage() {
  const data = Route.useLoaderData()
  const { user } = Route.useRouteContext()

  return <Dashboard data={data} user={user} />
}

Good Example: Role-Based Access

// routes/_admin.tsx
export const Route = createFileRoute('/_admin')({
  beforeLoad: async ({ context }) => {
    // context.user comes from parent _authenticated route
    if (context.user.role !== 'admin') {
      throw redirect({ to: '/unauthorized' })
    }
  },
  component: AdminLayout,
})

// File structure:
// routes/
//   _authenticated.tsx        # Requires login
//   _authenticated/
//     dashboard.tsx           # /dashboard - any authenticated user
//     settings.tsx            # /settings - any authenticated user
//     _admin.tsx              # Admin layout
//     _admin/
//       users.tsx             # /users - admin only
//       analytics.tsx         # /analytics - admin only

Good Example: Preserving Redirect URL

// routes/login.tsx
import { z } from 'zod'

export const Route = createFileRoute('/login')({
  validateSearch: z.object({
    redirect: z.string().optional(),
  }),
  component: LoginPage,
})

function LoginPage() {
  const { redirect } = Route.useSearch()
  const loginMutation = useMutation({
    mutationFn: login,
    onSuccess: () => {
      // Redirect to original destination or default
      navigate({ to: redirect ?? '/dashboard' })
    },
  })

  return <LoginForm onSubmit={loginMutation.mutate} />
}

// In protected routes
beforeLoad: async ({ location }) => {
  if (!session) {
    throw redirect({
      to: '/login',
      search: { redirect: location.href },
    })
  }
}

Good Example: Conditional Content Based on Auth

// Public route with different content for logged-in users
export const Route = createFileRoute('/')({
  beforeLoad: async () => {
    const session = await getSessionData()
    return { user: session?.user ?? null }
  },
  component: HomePage,
})

function HomePage() {
  const { user } = Route.useRouteContext()

  return (
    <div>
      <Hero />
      {user ? (
        <PersonalizedContent user={user} />
      ) : (
        <SignUpCTA />
      )}
    </div>
  )
}

Context

  • beforeLoad runs before route loading begins
  • Throwing redirect() prevents route from loading
  • Context from beforeLoad flows to loader and component
  • Child routes inherit parent's beforeLoad protection
  • Use pathless layout routes (_authenticated.tsx) for grouped protection
  • Store redirect URL in search params for post-login navigation