Hi all ๐
Currently integrating a Tanstack Start + React query app with Better auth.
This is my setup at the moment:
src/features/auth/fn/get-user.ts
import { createServerFn } from "@tanstack/react-start";
import { getRequest, setResponseHeader } from "@tanstack/react-start/server";
import { auth } from "@/lib/auth";
export const getUser = createServerFn({ method: "GET" }).handler(async () => {
const session = await auth.api.getSession({
headers: getRequest().headers,
returnHeaders: true,
});
// Forward any Set-Cookie headers to the client, e.g. for session/cache refresh
const cookies = session.headers?.getSetCookie();
if (cookies?.length) {
setResponseHeader("Set-Cookie", cookies);
}
return session.response?.user || null;
});
query-options.ts:
import { queryOptions } from "@tanstack/react-query";
import { getUser } from "../fn/get-user";
import { AUTH_KEYS } from "./query-keys";
export const authQueryOptions = () =>
queryOptions({
queryKey: AUTH_KEYS.user,
queryFn: ({ signal }) => getUser({ signal }),
staleTime: 1000 * 60 * 5, // 5 minutes - auth data doesn't change often
gcTime: 1000 * 60 * 10, // 10 minutes in cache
});
export type AuthQueryResult = Awaited<ReturnType<typeof getUser>>;
src/middleware/route-auth.ts
export const authMiddleware = createMiddleware().server(async ({ next }) => {
const headers = getRequestHeaders();
const session = await auth.api.getSession({ headers });
if (!session) {
throw redirect({ to: "/login" });
}
return await next({
context: {
headers,
session,
user: session.user,
},
});
});
src/routes/_protected/route.tsx
export const Route = createFileRoute("/_protected")({
component: PlatformLayout,
server: {
middleware: [authMiddleware],
},
beforeLoad: async ({ context }) => {
const user = await context.queryClient.ensureQueryData({
...authQueryOptions(),
revalidateIfStale: true,
});
if (!user) {
throw redirect({ to: "/login" });
}
// re-return to update type as non-null for child routes
return { user };
},
});
Guest routes protection:
// For routes like /login, /signup
beforeLoad: async ({ context }) => {
const user = await context.queryClient.ensureQueryData({
...authQueryOptions(),
revalidateIfStale: true,
});
if (user) {
throw redirect({ to: "/dashboard" }); // redirect authenticated users away
}
},
useAuth hook:
export const useAuth = () => {
const { data: user, isPending } = useQuery(authQueryOptions());
return { user, isPending };
};
// Usage in components
const { user } = useAuth();
I obviously still have my server.middleware set to [authMiddleware] in the Route, so if it happens on the server, it's protected.
Then the beforeLoad stuff protects on the client side.
Is this the correct/recommended way tho? Am I missing or overcomplicating something? ๐ค
