My project was created using vite + react-js.
Let's get the required keys from Google Cloud Console. I have these two URLs form which I'll be sending the google sign-in request from Local host i.e. http://localhost:5173 and a Production URL https://MY_DOMAIN_NAME.com.
Go to Google Cloud Console create/select a new proejct → in search bar search OAuth consent screen and select it:
In Branding you'll need to add you https://MY_DOMAIN_NAME.com in the Authorized domains section:
You can keep the app in test mode or Publish App. I'll keep it in test mode and click on + Add users towards the end under Test users. These users will be allowed to login into the app only.
If you decides to publish you'll need to have links for privacy policy and terms of service to your website.
Click Clients on the left sidebar. Click Create OAuth client.
Fill out name of your project
Authorized redirect URIs: Paste your exact Supabase callback URL. It looks exactly like this:
https://<YOUR_PROJECT_ID>.supabase.co/auth/v1/callback
You can find the ...supabase.co/auth/v1/... from Supabase Dashboard -> Authentication -> Sign In / Providers > Google
Click Create. Copy the Client ID and Client Secret it gives you.
--
Next, back in your Supabase Dashboard -> Authentication -> Sign In / Providers > Google, toggle it on.
Paste the Client ID and Client Secret you just created in GCP, and click Save.
Next, Go to Authentication -> URL Configuration.
Site URL: Enter https://MY_DOMAIN_NAME.com
Redirect URLs: Click "Add URL" and add these two exact lines so auth works locally and in production:
http://localhost:5173/**https://MY_DOMAIN_NAME.com/**
I have the URL whitelisted in my Supabase project as well:
Below is how I'll implement it in my ReactJS project. I'll be using context API to keep the logged in state (feel free to use zustand or any you prefer).
Install this first:
yarn add @supabase/supabase-js
You should have your:
VITE_SUPABASE_URL=https://<YOUR_PROJECT_ID>.supabase.co
VITE_SUPABASE_ANON_KEY=your_long_anon_key_here
in your .env file. If you haven't check the Extras section at the end.
Add a file named supabaseClient.ts and in it add:
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.YOUR_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.YOUR_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error("Missing Supabase environment variables. Please set YOUR_PUBLIC_SUPABASE_URL and YOUR_SUPABASE_ANON_KEY.");
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
As I'm using Context API, we'll need to create three files to add a context for Auth:
AuthContext.tsxAuthProvider.tsxuseAuth.ts
In AuthContext.tsx add this code:
import type { Session, User } from "@supabase/supabase-js";
import { createContext } from "react";
export type UserProfile = {
id: string;
email: string | null;
full_name: string | null;
photo_url: string | null;
};
type AuthContextType = {
user: User | null;
profile: UserProfile | null;
session: Session | null;
loading: boolean;
signInWithGoogle: () => Promise<void>;
signOut: () => Promise<void>;
};
export const AuthContext = createContext<AuthContextType | null>(null);
The create a file AuthProvider.tsx add:
import type { Session, User } from "@supabase/supabase-js";
import { useEffect, useState } from "react";
import { supabase } from "../../lib/supabase";
import { AuthContext, type UserProfile } from "./AuthContext";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
// If you want to store user data in a supabase table as well, for example I have user_data table table example here in upsertProfile
const upsertProfile = async (u: User) => {
const data = {
id: u.id,
email: u.email ?? null,
full_name: u.user_metadata?.full_name ?? null,
photo_url: u.user_metadata?.avatar_url ?? null,
};
const { data: saved } = await supabase
.from("user_data")
.upsert(data, { onConflict: "id" })
.select()
.single();
setProfile(saved ?? data);
};
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
if (session?.user) upsertProfile(session.user);
setLoading(false);
});
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
setUser(session?.user ?? null);
if (session?.user) upsertProfile(session.user);
else setProfile(null);
});
return () => subscription.unsubscribe();
}, []);
const signInWithGoogle = async () => {
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: window.location.origin },
});
};
const signOut = async () => {
await supabase.auth.signOut();
};
return (
<AuthContext.Provider
value={{ user, profile, session, loading, signInWithGoogle, signOut }}
>
{children}
</AuthContext.Provider>
);
}
and finally the hook, create a file useAuth.ts and in it add:
import { useContext } from "react";
import { AuthContext } from "./AuthContext.tsx";
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
Now you are finally ready to add the changes to app. I have seen people putting the auth context directly in App.tsx, or there are other mechanisms as well, so it depends on your app specific needs.
In this article, I'll take a simple approach, we'll wrap the whole <App /> in the AuthProvider so open main.tsx that has your app's ...<StrictMode><App />... and wrap the <App /> with the AuthProvider:
//... Your imports
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>,
);
Your routing will be in the App.tsx file. As an example say you have two pages Home.tsx and Profile.tsx.
-
Home.tsxcan be accessed by both authenticated and not authenticated users. Some parts of the pages will be available for public. -
Profile.tsxOnly accessible by the Authenticated users
For Home.tsx
import { useAuth } from "./AuthProvider";
//... other imports
export default function Home() {
const {
user,
profile,
isLoading: isAuthLoading,
signInWithGoogle,
signOut,
} = useAuth();
//... other code
return (
<div className="flex items-center justify-between gap-4">
<h1>Home</h1>
<p>This page is public.</p>
{user && profile ? (
<div>
<p>Welcome, {profile.name}!</p>
<p>{profile.email}</p>
<p>{profile.avatarUrl}</p>
<button onClick={signOut}>Sign Out</button>
</div>
) : (
<button onClick={() => {
void signInWithGoogle();
}}>Login with Google</button>
)}
</div>
)
}
For Profile.tsx:
import { useNavigate } from "react-router-dom";
//... Other imports
export default function Profile() {
const { user, loading } = useAuth();
const navigate = useNavigate();
useEffect(() => {
if (!loading && !user) navigate("/", { replace: true });
}, [user, loading, navigate]);
if (loading || !user) return null;
return <div>Profile</div>;
}
NOTE: Using the above approach in Profile.tsx, is a common and safe pattern. The only edge case to know about: There's a brief moment where an unauthenticated user could see the page render before the redirect fires — your if (loading || !user) return null handles that by rendering nothing until auth state is resolved, so nothing sensitive is ever shown.
The loading guard is the key safety net here. As long as you return null while loading and redirect when !user, there's no flash of protected content.
Extras
Getting Supabase Anon Key and other required variables for .env
In Supabse project goto Project Settings and then API Keys
The default key under Publishable key That is your new "anon" key, Copy that top one and paste it into your .env as VITE_SUPABASE_ANON_KEY.
(You don't need the "Secret key" anywhere so leave it as is).
To get VITE_SUPABASE_URL click on Data API under INTEGRATIONS menu:
This is what you'll paste for your VITE_SUPABASE_URL.











Top comments (0)