Skip to content

Instantly share code, notes, and snippets.

@Mingling94
Created May 8, 2026 02:48
Show Gist options
  • Select an option

  • Save Mingling94/062b34a9f314d2a7bb44355f1cad6c1a to your computer and use it in GitHub Desktop.

Select an option

Save Mingling94/062b34a9f314d2a7bb44355f1cad6c1a to your computer and use it in GitHub Desktop.
React + Supabase test-mode auth session pattern

React + Supabase Test-Mode Auth Session Pattern

This is a small public reference for a focused React + Supabase auth persistence bug.

Goal:

  • Keep Supabase Auth as the real production auth provider.
  • Add deterministic test-mode auth only for staging/test environments.
  • Avoid writing fake tokens into Supabase's own auth storage.
  • Let route guards recognize either a real Supabase session or a separately stored test-mode session.
  • Preserve reload behavior for Playwright and manual staging tests.

Important boundary: This is not meant to bypass production auth. In a real app, test mode should be guarded by build-time environment flags and deployment/origin checks, and production builds should fail closed.

Core idea:

  • Real session: comes from supabase.auth.getSession() and supabase.auth.onAuthStateChange(...).
  • Test session: app-owned object in a separate localStorage key.
  • Route guard: accepts real auth first, then test auth only when explicitly enabled.
  • Logout: clears the app-owned test session and calls Supabase sign-out for real sessions.

Relevant Supabase docs:

import * as React from "react";
import { Navigate, Outlet, useNavigate } from "react-router-dom";
import type { Session, SupabaseClient, User } from "@supabase/supabase-js";
const TEST_MODE_STORAGE_KEY = "app:test-mode-auth:v1";
const TEST_MODE_TTL_MS = 60 * 60 * 1000;
type AuthState =
| { status: "loading" }
| { status: "anonymous" }
| { status: "authenticated"; source: "supabase"; session: Session; user: User }
| { status: "authenticated"; source: "test-mode"; session: TestModeSession };
type TestModeSession = {
kind: "test-mode";
user: {
id: string;
email: string;
role: "student" | "teacher" | "admin";
};
createdAt: number;
expiresAt: number;
};
type AuthContextValue = {
auth: AuthState;
enterTestMode: () => void;
signOut: () => Promise<void>;
};
const AuthContext = React.createContext<AuthContextValue | null>(null);
export function AuthProvider({
children,
supabase,
}: {
children: React.ReactNode;
supabase: SupabaseClient;
}) {
const [auth, setAuth] = React.useState<AuthState>({ status: "loading" });
const navigate = useNavigate();
React.useEffect(() => {
let active = true;
supabase.auth.getSession().then(({ data, error }) => {
if (!active) return;
if (error) {
setAuth(readValidTestModeSession() ?? { status: "anonymous" });
return;
}
const session = data.session;
if (session?.user) {
clearTestModeSession();
setAuth({ status: "authenticated", source: "supabase", session, user: session.user });
return;
}
setAuth(readValidTestModeSession() ?? { status: "anonymous" });
});
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
if (!active) return;
if (session?.user) {
clearTestModeSession();
setAuth({ status: "authenticated", source: "supabase", session, user: session.user });
return;
}
if (event === "SIGNED_OUT") {
setAuth({ status: "anonymous" });
return;
}
if (event === "INITIAL_SESSION") {
setAuth(readValidTestModeSession() ?? { status: "anonymous" });
}
});
return () => {
active = false;
subscription.unsubscribe();
};
}, [supabase]);
const enterTestMode = React.useCallback(() => {
const session = createTestModeSession();
writeTestModeSession(session);
setAuth({ status: "authenticated", source: "test-mode", session });
navigate("/dashboard", { replace: true });
}, [navigate]);
const signOut = React.useCallback(async () => {
clearTestModeSession();
await supabase.auth.signOut();
setAuth({ status: "anonymous" });
navigate("/auth", { replace: true });
}, [navigate, supabase]);
return <AuthContext.Provider value={{ auth, enterTestMode, signOut }}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used inside AuthProvider");
}
return context;
}
export function ProtectedRoute() {
const { auth } = useAuth();
if (auth.status === "loading") {
return null;
}
if (auth.status === "authenticated") {
return <Outlet />;
}
return <Navigate to="/auth" replace />;
}
function createTestModeSession(): TestModeSession {
assertTestModeEnabled();
const now = Date.now();
return {
kind: "test-mode",
user: {
id: "test-user",
email: "test-user@example.invalid",
role: "student",
},
createdAt: now,
expiresAt: now + TEST_MODE_TTL_MS,
};
}
function readValidTestModeSession(): AuthState | null {
if (!isTestModeEnabled()) return null;
const raw = window.localStorage.getItem(TEST_MODE_STORAGE_KEY);
if (!raw) return null;
try {
const session = JSON.parse(raw) as TestModeSession;
if (session.kind !== "test-mode" || session.expiresAt <= Date.now()) {
clearTestModeSession();
return null;
}
return { status: "authenticated", source: "test-mode", session };
} catch {
clearTestModeSession();
return null;
}
}
function writeTestModeSession(session: TestModeSession) {
window.localStorage.setItem(TEST_MODE_STORAGE_KEY, JSON.stringify(session));
}
function clearTestModeSession() {
window.localStorage.removeItem(TEST_MODE_STORAGE_KEY);
}
function assertTestModeEnabled() {
if (!isTestModeEnabled()) {
throw new Error("Test mode auth is disabled in this build.");
}
}
function isTestModeEnabled() {
return import.meta.env.MODE !== "production" && import.meta.env.VITE_ENABLE_TEST_MODE === "true";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment