This episode secures your TanStack app: secure token storage, auth headers on the fetcher, the refresh token flow, router guards with beforeLoad, and role-based data fetching and access control.

Your queries and mutations now carry valuable data. It's time to secure them. Authentication determines who the user is, and secure data fetching determines what data each person is allowed to get. TanStack doesn't ship built-in auth — but it provides all the hooks you need to integrate it.
Episode 12 covers secure token storage, auth headers on the fetcher, the refresh token flow, router guards, and role-based data fetching. These patterns build a security layer around your queries and routes.
The core principle is simple: tokens are guarded tightly, every request carries the right credentials, and access is checked in two places — the router for navigation, and query functions for data.
The safest way to store an access token is an HTTPOnly cookie set by the server. Cookies can't be read by JavaScript, making them immune to XSS attacks that steal tokens from localStorage:
Set-Cookie: access_token=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900HttpOnly; Secure; SameSite=Strict makes the cookie sent only over HTTPS, never sent cross-site, and inaccessible to browser code. If you use localStorage because of backend constraints, minimize token lifetime and always validate input so XSS can't reach it.
Instead of writing the Authorization header in every query, wrap it into a single fetcher. All query functions use this fetcher:
async function authFetch(url, options = {}) {
const token = await getToken()
const res = await fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
...options.headers,
},
})
if (res.status === 401) {
await refreshToken()
return authFetch(url, options)
}
return res
}
const { data } = useQuery({
queryKey: ["profil"],
queryFn: () => authFetch("/api/profil").then((res) => res.json()),
})authFetch injects Authorization and handles 401 transparently. When the token is stale, it calls refreshToken, then retries the request. Query functions stay clean — the auth complexity is hidden in one place.
A refresh token has a long lifetime and is used only to obtain new access tokens. The correct flow: the access token expires, the request returns 401, a new token is fetched, and the original request is retried:
let refreshPromise = null
async function refreshToken() {
if (!refreshPromise) {
refreshPromise = fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
}).then((res) => res.json())
}
return refreshPromise.finally(() => {
refreshPromise = null
})
}refreshPromise holds one shared refresh process. If several requests 401 at the same time, they all wait on the same promise instead of triggering repeated refreshes — preventing a flood of requests and race conditions.
TanStack Router protects routes through beforeLoad. If the condition isn't met, throw redirect to move the user elsewhere:
import { redirect } from "@tanstack/react-router"
const dashboardRoute = createRoute({
getParentRoute: () => rootRoute,
path: "dashboard",
beforeLoad: ({ context }) => {
if (!context.auth.isAuthenticated) {
throw redirect({ to: "/login" })
}
},
component: DashboardComponent,
})context.auth comes from the router context populated at login. throw redirect({ to: "/login" }) halts navigation and moves the user. The guard can be mounted on a parent route so it protects all its children at once.
Auth isn't enough at the data level. Combine route guards with query functions that adapt to the user's role:
const adminRoute = createRoute({
getParentRoute: () => rootRoute,
path: "admin",
beforeLoad: ({ context }) => {
if (context.auth.role !== "admin") {
throw redirect({ to: "/dashboard" })
}
},
component: AdminComponent,
})The guard ensures only users with the admin role can open the admin page. On the data side, query functions use the role as part of the queryKey, and authorization is still verified by the server. Router guards only shape the experience; the server is the final gatekeeper.
Warning
Front-end guards are never enough for security. Always enforce authorization on the backend, because users can manipulate browser code. TanStack Router guards are only UX convenience.
Episode 12 secured your app: tokens stored in HTTPOnly cookies, authFetch carrying credentials and handling 401, refresh tokens deduplicated with a shared promise, routes guarded through beforeLoad, and access restricted per role.
Key takeaways:
In the next episode, episode 13, we'll discuss API integration and caching strategy — REST and GraphQL integration, cache strategy with staleTime and gcTime, invalidation for real-time data, and offline support with cache persistence. Your queries will learn to talk to different backends!