In React Router, you can use middleware to create a server-side Supabase client, refresh the session once per request, and write updated auth cookies back to the response. This ensures that loaders and actions have access to a valid session.
- Create a Context: Define a context to hold the
SupabaseClient. - Implement Middleware: Use
createServerClient along with parseCookieHeader and serializeCookieHeader to manage cookies within the middleware lifecycle. - Attach Middleware: Apply the middleware to a parent route so child loaders can retrieve the client from the context.
// app/context.ts
import { createContext } from "react-router";
import type { SupabaseClient } from "@supabase/supabase-js";
export const supabaseContext = createContext<SupabaseClient | null>(null);
// app/middleware/supabase.ts
import {
createServerClient,
parseCookieHeader,
serializeCookieHeader,
} from "@supabase/ssr";
import type { CookieOptions } from "@supabase/ssr";
import type { MiddlewareFunction } from "react-router";
import { supabaseContext } from "~/context";
type PendingCookie = {
name: string;
value: string;
options: CookieOptions;
};
export const supabaseMiddleware: MiddlewareFunction<Response> = async (
{ request, context },
next,
) => {
const pendingCookies: PendingCookie[] = [];
const pendingHeaders: Record<string, string> = {};
const supabase = createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return parseCookieHeader(request.headers.get("Cookie") ?? "");
},
setAll(cookiesToSet, headers) {
pendingCookies.push(...cookiesToSet);
Object.assign(pendingHeaders, headers);
},
},
},
);
// Trigger lazy session init / refresh before any route code runs.
await supabase.auth.getClaims();
context.set(supabaseContext, supabase);
const response = await next();
for (const { name, value, options } of pendingCookies) {
response.headers.append(
"Set-Cookie",
serializeCookieHeader(name, value, options),
);
}
for (const [key, value] of Object.entries(pendingHeaders)) {
response.headers.set(key, value);
}
return response;
};
// app/routes/home.tsx
import type { Route } from "./+types/home";
import { supabaseMiddleware } from "~/middleware/supabase";
import { supabaseContext } from "~/context";
export const middleware: Route.MiddlewareFunction[] = [supabaseMiddleware];
export async function loader({ context }: Route.LoaderArgs) {
const supabase = context.get(supabaseContext);
const { data } = await supabase!.auth.getClaims();
return { claims: data?.claims ?? null };
}