Auth: Google Sign-In + Roles
Set up Google OAuth with Supabase, role-based access, and protected routes in Next.js 16.
Table of content
- Auth: Google Sign-In + Roles
- Step 1 — Enable Google in Supabase Dashboard
- Step 2 — Install Supabase SSR
- Step 3 — Add the Proxy File
- Step 4 — Login Page
- Step 5 — Auth Callback Route
-
Step 6 — Create the
profilesTable + Role System - Step 7 — Update RLS on Products + Storage
- Step 8 — Add Sign Out + User Info to the Nav
- What You've Built
Auth: Google Sign-In + Roles
Step 1 — Enable Google in Supabase Dashboard
~5 min — No code needed
- Supabase Dashboard → Authentication → Providers → Google → toggle ON
- Go to console.cloud.google.com → APIs & Services → Credentials → Create OAuth 2.0 Client ID
- Set Authorized redirect URI to:
https://YOUR-PROJECT-REF.supabase.co/auth/v1/callbackFind your project ref in Supabase → Project Settings → General
- Copy Client ID and Client Secret → paste back into Supabase
Step 2 — Install Supabase SSR
npm install @supabase/ssr
Skip if you already have
@supabase/ssrfrom previous sessions.
Step 3 — Add the Proxy File
⚠️ In Next.js 16,
middleware.tsis renamed toproxy.tsand the function export renamed toproxy.
proxy.ts (project root — replaces middleware.ts):
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function proxy(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
},
);
// Refresh session
const {
data: { user },
} = await supabase.auth.getUser();
const path = request.nextUrl.pathname;
// Protect /admin — admins only
if (path.startsWith("/admin")) {
if (!user) return NextResponse.redirect(new URL("/login", request.url));
const { data: profile } = await supabase
.from("profiles")
.select("role")
.eq("id", user.id)
.single();
if (profile?.role !== "admin")
return NextResponse.redirect(new URL("/", request.url));
}
// Protect /orders — must be signed in
if (path.startsWith("/orders") && !user)
return NextResponse.redirect(new URL("/login", request.url));
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
Step 4 — Login Page
app/login/page.tsx:
"use client";
import { createClient } from "@/utils/supabase/client";
export default function LoginPage() {
const supabase = createClient();
async function signIn() {
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: `${location.origin}/auth/callback` },
});
}
return (
<main className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-950">
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-10 w-full max-w-sm flex flex-col gap-6">
<h1 className="text-xl font-bold text-center">Sign In</h1>
<button
onClick={signIn}
className="flex items-center justify-center gap-3 border border-gray-300 dark:border-gray-700 rounded-lg py-2.5 font-medium text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition"
>
<img
src="https://www.svgrepo.com/show/475656/google-color.svg"
className="w-5 h-5"
/>
Continue with Google
</button>
</div>
</main>
);
}
Step 5 — Auth Callback Route
Exchanges the OAuth code for a session cookie.
app/auth/callback/route.ts:
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get("code");
// In Next.js 16, cookies() is async — always await it
const cookieStore = await cookies();
if (code) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (c) =>
c.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options),
),
},
},
);
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL("/", request.url));
}
Step 6 — Create the profiles Table + Role System
Run once in Supabase → SQL Editor:
-- 1. Profiles table (mirrors auth.users)
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
email text,
role text not null default 'user' -- 'user' or 'admin'
);
alter table public.profiles enable row level security;
create policy "Read own profile"
on public.profiles for select
using (auth.uid() = id);
-- 2. Auto-create profile on every sign-up
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer as $$
begin
insert into public.profiles (id, email)
values (new.id, new.email);
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
To make yourself an admin, run this after signing in once:
update public.profiles set role = 'admin' where email = 'you@gmail.com';
Step 7 — Update RLS on Products + Storage
Replaces the open "public insert" policies from the previous session.
-- Drop the old open policies
drop policy if exists "Public insert" on public.products;
drop policy if exists "Public upload images" on storage.objects;
-- Only admins can add products
create policy "Admin insert products"
on public.products for insert
with check (
exists (
select 1 from public.profiles
where id = auth.uid() and role = 'admin'
)
);
-- Only admins can upload images
create policy "Admin upload images"
on storage.objects for insert
with check (
bucket_id = 'product-images' and
exists (
select 1 from public.profiles
where id = auth.uid() and role = 'admin'
)
);
Your existing Public read policies for products and images stay as-is.
Step 8 — Add Sign Out + User Info to the Nav
components/UserNav.tsx:
"use client";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
export default function UserNav({ email }: { email: string | null }) {
const supabase = createClient();
const router = useRouter();
async function signOut() {
await supabase.auth.signOut();
router.refresh();
}
if (!email)
return (
<a
href="/login"
className="text-sm font-medium text-blue-500 hover:underline"
>
Sign In
</a>
);
return (
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500 dark:text-gray-400">
{email}
</span>
<button
onClick={signOut}
className="text-sm text-gray-600 dark:text-gray-400 hover:text-red-500 transition"
>
Sign out
</button>
</div>
);
}
In your products page (server component), fetch the user and pass the email:
// fetch user at the top of the server component
const { data: { user } } = await supabase.auth.getUser()
// in your nav JSX
<UserNav email={user?.email ?? null} />
What You've Built
| Google OAuth | signInWithOAuth → callback route → session cookie |
| Profiles table | Auto-created on sign-up via DB trigger |
| Roles | profiles.role = 'user' | 'admin' |
/admin protection |
proxy.ts checks role, redirects non-admins |
/orders protection |
proxy.ts redirects unauthenticated users |
| RLS on products | Admin-only insert (was open to everyone) |